The Sweep
A Sweep operation extrudes a 2D Profile Curve along a 3D Rail Curve (also called a path). The Rail defines the trajectory, and the Profile defines the cross-section of the resulting 3D geometry.
Sweep Inputs:
1# Defining curves for Sweep2# A sweep requires at least two inputs:3# 1. Rail Curve: The path the shape follows.4# 2. Profile Curve: The cross-section shape to extrude.56def define_sweep_inputs():7 rail = NurbsCurve(points=[(0,0,0), (1,2,0), (2,1,2)])8 profile = Polygon(radius=0.5, sides=6) # Hexagon910 return rail, profile
To move the profile along the rail, we need a local coordinate system (a frame) at every point. The Frenet-Serret formulas provide a mathematical way to define this frame using the curve's derivatives. However, this frame twists wildly when the curve's curvature changes direction (inflection points).
Frenet Frames:
1# Frenet-Serret Frame Calculation2def get_frenet_frame(curve, t):3 """Calculates T, N, B vectors at parameter t."""45 # Tangent (1st derivative)6 T = curve.derivative(t).normalize()78 # Normal (2nd derivative)9 N = curve.second_derivative(t).normalize()1011 # Binormal (Cross product)12 B = T.cross(N).normalize()1314 return T, N, B
To avoid the flipping and twisting of Frenet frames, CAD software usually uses Parallel Transport (Rotation Minimizing Frames). It calculates the first frame, and then sequentially rotates it just enough to align with the new tangent at each step, minimizing any twist around the axis.
Parallel Transport:
1# Parallel Transport Frame (Rotation Minimizing Frame)2def parallel_transport(curve, points):3 """Generates twist-free frames along a curve."""4 frames = [initial_frenet_frame(curve, 0)]56 for i in range(1, len(points)):7 # Get previous frame and current/next tangents8 prev_frame = frames[i-1]9 t0 = prev_frame.tangent10 t1 = get_tangent(curve, points[i])1112 # Axis of rotation between tangents13 axis = t0.cross(t1)14 angle = math.acos(t0.dot(t1))1516 # Rotate previous Normal/Binormal around Axis by Angle17 new_frame = rotate_frame(prev_frame, axis, angle)18 frames.append(new_frame)1920 return frames
With the twist-free frames calculated along the rail, we map the 2D coordinates of the Profile Curve onto the 3D axes (Normal and Binormal) of each frame. This places a copy of the profile at every evaluated point.
Profile Mapping:
1# Orienting Profile along Rail2def sweep_profiles(rail, profile, frames):3 """Orients the profile 2D points to the 3D frames."""4 oriented_profiles = []56 for frame in frames:7 current_profile = []8 for point_2d in profile:9 # Map 2D x,y to the frame's Normal and Binormal vectors10 # Map origin to frame.position11 p_3d = frame.position + (frame.normal * point_2d.x) + (frame.binormal * point_2d.y)1213 current_profile.append(p_3d)1415 oriented_profiles.append(current_profile)1617 return oriented_profiles
A standard sweep keeps the profile size constant. By applying a mathematical function (or a secondary "scale rail") to the profile's 2D coordinates before orienting them, we can dynamically vary the thickness of the sweep along its length.
Dynamic Scaling:
1# Adding Scale Variation2def sweep_with_scale(rail, profile, frames, scale_factor=1.0):3 """Applies dynamic scaling along the rail."""4 oriented_profiles = []56 for i, frame in enumerate(frames):7 # Calculate t parameter [0.0 to 1.0]8 t = i / (len(frames) - 1)910 # Example: Scale pulses based on a sine wave11 current_scale = 1.0 + math.sin(t * math.pi * 4) * scale_factor1213 current_profile = []14 for point_2d in profile:15 # Apply scale before frame orientation16 sx = point_2d.x * current_scale17 sy = point_2d.y * current_scale1819 p_3d = frame.position + (frame.normal * sx) + (frame.binormal * sy)20 current_profile.append(p_3d)2122 oriented_profiles.append(current_profile)2324 return oriented_profiles
Once we have the mathematical coordinates of the profiles positioned along the rail, we must weave them together to form a solid surface. We do this by connecting adjacent points on adjacent profiles to form Quads (which are split into two triangles for rendering).
Meshing:
P[i][j] to P[i][j+1], P[i+1][j+1], and P[i+1][j]. 3. Ensure the last point of a closed profile connects back to the first point.1# Generating the Skin (Mesh Topology)2def generate_sweep_mesh(profiles):3 """Stitches oriented profiles together into a quad mesh."""4 vertices = []5 faces = []67 num_profiles = len(profiles)8 num_points = len(profiles[0])910 # 1. Flatten all points into a single vertex list11 for profile in profiles:12 vertices.extend(profile)1314 # 2. Connect vertices to form quads (2 triangles)15 for i in range(num_profiles - 1):16 for j in range(num_points):17 # Calculate indices for the quad corners18 current = (i * num_points) + j19 next_pt = (i * num_points) + ((j + 1) % num_points) # Wrap around2021 above = current + num_points22 above_next = next_pt + num_points2324 # Triangle 1: current, next, above25 faces.append([current, next_pt, above])26 # Triangle 2: next, above_next, above27 faces.append([next_pt, above_next, above])2829 return Mesh(vertices, faces)
If the Profile Curve is a closed shape, the resulting Sweep will be a tube with open ends. To turn it into a solid, watertight manifold (a "Brep" or Solid Mesh), we must generate "Caps" at the first and last profiles.
Capping algorithm:
1# Capping the Sweep2def generate_cap(profile_points, reverse_normals=False):3 """Creates a planar cap for the open ends of the sweep."""4 # 1. Calculate centroid of the profile5 centroid = calculate_centroid(profile_points)67 vertices = [centroid] + profile_points8 faces = []910 # 2. Create triangle fan from centroid to edge points11 num_pts = len(profile_points)12 for i in range(num_pts):13 p1 = i + 114 p2 = ((i + 1) % num_pts) + 11516 # Reverse winding order for the start cap so normals face outwards17 if reverse_normals:18 faces.append([0, p2, p1])19 else:20 faces.append([0, p1, p2])2122 return Mesh(vertices, faces)