Closed KilowattSynthesis closed 3 weeks ago
It seems like the fundamental problem is "When sweeping over a polyline, I can't find an elegant solution to keep the section normal to the path always." - the location_at/^
method/operation will find the exact position/orientation anywhere along an Edge
/Wire
. Please provide an example of where this fails.
One problem that users have is that the rotation around the Edge
/Wire
provided by location_at
may not be what one expects resulting in difficulties when attempting to manually continue a sweep. A sweep_normal
operation wouldn't solve this problem though.
Sorry this is such a long example (ie not a "minimum reproducable example"), but it's what I've been working with, so take a look if you can! Thank you for the help :)
"""Create CAD model example of custom helix with variable radius extrude not working."""
import math
import os
from dataclasses import dataclass
import build123d as bd
from ocp_vscode import show
@dataclass
class ScrewSpec:
"""Specification for wavy ball screw."""
ball_od: float = 1.588 + 0.1
screw_pitch: float = 2.5
screw_od: float = 2.4 # Just under 2.5mm
screw_length: float = 2.5 * 4
ball_points_per_turn: int = 32
# Define the varying radius function
def radius_function(
z: float, *, pitch: float, start_radius: float, max_radius: float
) -> float:
"""Calculate the radius of a helix with a periodically varying radius."""
# TODO: Consider adding an argument to support various functions,
# like sine instead of linear.
# Calculate position within a single pitch period
z_normalized = z % pitch
if z_normalized <= pitch / 2:
return start_radius + (max_radius - start_radius) * (2 * z_normalized / pitch)
# else:
return max_radius - (max_radius - start_radius) * (
2 * (z_normalized - pitch / 2) / pitch
)
def insane_helix_points(
*,
start_radius: float,
max_radius: float,
pitch: float,
height: float,
points_per_turn: int = 100,
) -> list[tuple[float, float, float]]:
"""Generate points along a helix with a periodically varying radius."""
# Number of turns
num_turns = height / pitch
# Generate points along the helix with varying radius
points = []
for i in range(int(num_turns * points_per_turn) + 1):
z = i * pitch / points_per_turn
theta = 2 * math.pi * z / pitch
r = radius_function(
z=z,
pitch=pitch,
start_radius=start_radius,
max_radius=max_radius,
)
x = r * math.cos(theta)
y = r * math.sin(theta)
points.append((x, y, z))
return points
def make_wavy_screw(spec: ScrewSpec) -> bd.Part:
"""Make the wavy screw that can raise and lower the balls.
Lowest point will be where the center of the ball is at the
radius of the screw. The circumference of the ball is at the
radius of the screw, minus 0.1.
Relevant learning docs: https://build123d.readthedocs.io/en/latest/examples_1.html#handle
"""
p = bd.Part()
_min_radius = spec.ball_od / 2 + 0.1 # - 0.5
_max_radius = spec.screw_od / 2 + spec.ball_od / 2 - 0.2
helix_points = insane_helix_points(
start_radius=_min_radius,
max_radius=_max_radius,
pitch=spec.screw_pitch,
# On height, subtract ball_od to prevent jamming/wedging.
height=spec.screw_length - spec.ball_od,
points_per_turn=spec.ball_points_per_turn,
)
helix = bd.Polyline(*helix_points)
p += bd.sweep(
path=helix,
sections=(helix ^ 0) * bd.Circle(radius=spec.ball_od / 2),
# multisection=True,
)
# Debugging: Helpful demo view.
show(bd.Polyline(*helix_points))
return p
if __name__ == "__main__":
show(make_wavy_screw(ScrewSpec()))
The output is squished like this:
OCCT has a very hard time with the helix so you'll be fighting OCCT not build123d to get this done. But here is the code:
# path = Helix(5, 10, 4, cone_angle=10) # Fails
# path = Helix(5, 10, 4) # Fails
# path = Line((0, 0, 0), (10, 0, 0)) # Passes
path = CenterArc((0, 0), 20, start_angle=0, arc_size=90) # Passes
profile_count = 40
profiles = [
(path ^ (i / profile_count)) * Circle(1 + i / profile_count)
for i in range(profile_count + 1)
]
screw = sweep(profiles, path, multisection=True)
However, I've found that if you break a helix up into small segments (less than one full revolution) it works a lot better (this is how thread is built):
path = Helix(5, 2.5, 4, cone_angle=10)
profile_count = 10
profiles = [
(path ^ (i / profile_count)) * Circle(1 + i / profile_count)
for i in range(profile_count + 1)
]
screw = sweep(profiles, path, multisection=True)
Note that my example above doesn't vary the radius of the circle, but rather of the spiral itself (i.e., the radius of the helix, not the circular sections). I can't use the helix function as the helix function has a constant radius.
Maybe I need to use arcs instead of line segments. Why can't line segments used to make a helix and then sweep it though? It seems like this should be possible.
Is there an issue we can open on OCCT if that's where the problems are? Any idea what the problem on that side is, in words?
path = Helix(5, 10, 4, cone_angle=20)
profile = (path ^ 0) * Circle(1)
screw = sweep(profile, path)
The cone_angle
parameter varies the radius of the Helix.
Any Edge
/Wire
can be used as the pah of the sweep, including a Polyline
but it won't be smooth. Use a Spline
if you want a smooth sweep. Splines
can easily be joined together by specifying the tangents - s1, s2 (with tangents (s1 % 1, something), s3 (tangents=(s2%1, something), etc..
I still don't understand what actual problem you have.
Here is an example of using straight line segments like those of a Polyline
:
path = RegularPolygon(10, 6).wire()
swept = sweep((path ^ 0) * Circle(1), path, transition=Transition.RIGHT)
Note that the transition
is set here - you might need to explore to find the best option for your specific use-case.
Hello,
Just as an input from a newbie regarding this matter:
When I looked at the introductory example 14, I was suprised that the Rectangle
swept along the path was not normal to the Line
considering it was normal to both JernArc
s.
In example 14 the path is created on Plane.XY
(the default) while the Rectangle
is created on Plane.XZ
which is perpendicular to Plane.XY
.
In example 14 the path is created on
Plane.XY
(the default) while theRectangle
is created onPlane.XZ
which is perpendicular toPlane.XY
.
I am not sure to get the point...
Shouldn't each tangent to the path be normal to the swept rectangle all along the path?
This seems the case along the two JenArc
s but not along the Line
part of the path which feels odd.
I would have said that the section all along the path should keep the dimensions of the Rectangle
.
But along the Line
part, the section is stretched because the swept Rectangle
is "tilted".
@gautaz I think this discussion is getting off topic from the original issue, but --
Between the arcs and the straight line segment there is a discontinuity. In most practical applications of sweep that I have seen the section is typically orthogonal to the path -- but that is NOT a requirement. The cross sectional area of the sweep along the line IS preserved, it is just that it is not orthogonal to the straight line path -- but rather parallel to Plane.YZ.
I personally do not support the overall feature request for this issue as I think it imposes too many assumptions about what a user wants.
I had tried lots of options with the transition
argument before, but it appears that it works as-expected this time. It seems that the transition
argument fulfills this feature request.
For sake of providing a minimum example of the technique I've been trying to do this whole time, see this example:
# ruff: noqa
import random
from ocp_vscode import show
import build123d as bd
def make_points() -> list[tuple[float, float, float]]:
random.seed(32) # Fix seed for reproducibility.
points = []
# Generate points moving rightward along the x-axis, with random y and z coordinates.
for i in range(6):
x = i * 10 # Increment x to move rightward
y = random.uniform(-5, 5)
z = random.uniform(-5, 5)
points.append((x, y, z))
return points
def main() -> bd.Part:
points = make_points()
path = bd.Polyline(points).wire()
part = bd.Part() + bd.sweep(
path=path,
sections=(path ^ 0) * bd.Circle(radius=1),
transition=bd.Transition.ROUND,
)
return part
show(main())
Note that the reason the cone_angle
argument isn't applicable is because the radius varies with an arbitrary function of height, and not just linearly. Also, aligning the ends of two cones with a cone_angle
is non-trivial, it seems.
Hello,
@gautaz I think this discussion is getting off topic from the original issue, but --
My apologies, this was not my intent.
Between the arcs and the straight line segment there is a discontinuity. In most practical applications of sweep that I have seen the section is typically orthogonal to the path -- but that is NOT a requirement. The cross sectional area of the sweep along the line IS preserved, it is just that it is not orthogonal to the straight line path -- but rather parallel to Plane.YZ.
@jdegenstein I was suspecting that this was related to the discontinuity, thanks for confirming this. So this explains the current behavior and it seems to relate to what @KilowattSynthesis was expecting when he asked for keeping the "section to be normal to the path" behavior.
I personally do not support the overall feature request for this issue as I think it imposes too many assumptions about what a user wants.
That is in fact up to the user to decide. But one thing that the user wants for sure is consistency in behavior. If the path is normal to the section for part of the path and not for the rest, it will always feel odd and mind bugging for most users.
@KilowattSynthesis Thanks for providing this polyline sample.
sections=(path ^ 0) * bd.Circle(radius=1)
seems bound to the fact that the section is a circle, what if the section is not circular?
Is there a way to generalize this technique?
Wouldn't the discontinuity be manageable with something like a loft between the section before the discontinuity and the section after it? (Sorry if this feels stupid, I am rather out of my depth here)
That is in fact up to the user to decide. But one thing that the user wants for sure is consistency in behavior. If the path is normal to the section for part of the path and not for the rest, it will always feel odd and mind bugging for most users.
The problem is that the software would need to make an assumption about what to do at the discontinuity -- that is a very non-trivial choice and way beyond the scope of the sweep functionality from OCCT that build123d uses. I am closing this issue as it appears it is resolved according to the OP.
Every time I've used the
sweep
method, I've struggled pretty hard. The docs don't make it clear that it only works "as-expected" if the swept section is right at the end of the path, and is normal to the path. I just learned about the caret operator, which helps, but it's still a bit verbose/ugly.When sweeping over a polyline, I can't find an elegant solution to keep the section normal to the path always. Perhaps I could create a bunch of sections (with iteration through the polyline), and then use
bd.sweep(..., multisection=True)
. Pretty verbose though.I propose that we add a method called
bd.sweep_normal(path=..., section=...)
, which will take a 2d section (e.g., abd.Circle
), and sweep it along the path, such that the normal of the circle always lines up with thepath
.Update 2024-11-10: Found a solution. Using the
transition=bd.Transition.ROUND
ortransition=bd.Transition.RIGHT
arguments makes the function work effectively as suggested above.