Skip to content

Commit b050614

Browse files
committed
-- Added connect_ends argument to solid.utils.extrude_along_path(), which will create closed continuous shapes if specified
-- Reworked endcap algorithm in solid.utils.extrude_along_path() so it works with *some* concave shapes -- Updated with examples of each feature.
1 parent b388e44 commit b050614

File tree

2 files changed

+127
-90
lines changed

2 files changed

+127
-90
lines changed

solid/examples/path_extrude_example.py

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,7 @@
1414

1515
SEGMENTS = 48
1616

17-
18-
def sinusoidal_ring(rad=25, segments=SEGMENTS):
19-
outline = []
20-
for i in range(segments):
21-
angle = radians(i * 360 / segments)
22-
scaled_rad = (1 + 0.18*cos(angle*5)) * rad
23-
x = scaled_rad * cos(angle)
24-
y = scaled_rad * sin(angle)
25-
z = 0
26-
# Or stir it up and add an oscillation in z as well
27-
# z = 3 * sin(angle * 6)
28-
outline.append(Point3(x, y, z))
29-
return outline
30-
31-
32-
def star(num_points=5, outer_rad=15, dip_factor=0.5):
33-
star_pts = []
34-
for i in range(2 * num_points):
35-
rad = outer_rad - i % 2 * dip_factor * outer_rad
36-
angle = radians(360 / (2 * num_points) * i)
37-
star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0))
38-
return star_pts
39-
40-
def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]:
41-
angles = [tau/num_points * i for i in range(num_points)]
42-
points = list([Point2(rad*cos(a), rad*sin(a)) for a in angles])
43-
return points
44-
45-
def extrude_example():
17+
def basic_extrude_example():
4618
path_rad = 50
4719
shape = star(num_points=5)
4820
path = sinusoidal_ring(rad=path_rad, segments=240)
@@ -81,11 +53,56 @@ def extrude_example_xy_scaling() -> OpenSCADObject:
8153
obj = no_scale_obj + right(3*path_rad)(x_obj) + right(6 * path_rad)(xy_obj)
8254
return obj
8355

56+
def extrude_example_capped_ends() -> OpenSCADObject:
57+
num_points = SEGMENTS/2
58+
path_rad = 50
59+
circle = star(6)
60+
path = circle_points(rad = path_rad)
61+
62+
# If `connect_ends` is False or unspecified, ends will be capped.
63+
# Endcaps will be correct for most convex or mildly concave (e.g. stars) cross sections
64+
capped_obj = translate([-path_rad / 2, 2 * path_rad])(text('Capped Ends'))
65+
capped_obj += extrude_along_path(circle, path, connect_ends=False)
66+
67+
# If `connect_ends` is specified, create a continuous manifold object
68+
connected_obj = translate([-path_rad / 2, 2 * path_rad])(text('Connected Ends'))
69+
connected_obj += extrude_along_path(circle, path, connect_ends=True)
70+
71+
return capped_obj + right(3*path_rad)(connected_obj)
72+
73+
def sinusoidal_ring(rad=25, segments=SEGMENTS):
74+
outline = []
75+
for i in range(segments):
76+
angle = radians(i * 360 / segments)
77+
scaled_rad = (1 + 0.18*cos(angle*5)) * rad
78+
x = scaled_rad * cos(angle)
79+
y = scaled_rad * sin(angle)
80+
z = 0
81+
# Or stir it up and add an oscillation in z as well
82+
# z = 3 * sin(angle * 6)
83+
outline.append(Point3(x, y, z))
84+
return outline
85+
86+
def star(num_points=5, outer_rad=15, dip_factor=0.5):
87+
star_pts = []
88+
for i in range(2 * num_points):
89+
rad = outer_rad - i % 2 * dip_factor * outer_rad
90+
angle = radians(360 / (2 * num_points) * i)
91+
star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0))
92+
return star_pts
93+
94+
def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]:
95+
angles = [tau/num_points * i for i in range(num_points)]
96+
points = list([Point2(rad*cos(a), rad*sin(a)) for a in angles])
97+
return points
98+
8499
if __name__ == "__main__":
85100
out_dir = sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent
86101

87-
basic_extrude = extrude_example()
102+
basic_extrude = basic_extrude_example()
88103
scaled_extrusions = extrude_example_xy_scaling()
89-
a = basic_extrude + translate([0,-250])(scaled_extrusions)
104+
capped_extrusions = extrude_example_capped_ends()
105+
a = basic_extrude + translate([0,-250])(scaled_extrusions) + translate([0, -500])(capped_extrusions)
106+
90107
file_out = scad_render_to_file(a, out_dir=out_dir, include_orig_code=True)
91108
print(f"{__file__}: SCAD file written to: \n{file_out}")

solid/utils.py

Lines changed: 79 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,8 @@ def path_2d_polygon(points:Sequence[Point23], width:float=1, closed:bool=False)
11431143
# ==========================
11441144
def extrude_along_path( shape_pts:Points,
11451145
path_pts:Points,
1146-
scale_factors:Sequence[Union[Vector2, float]]=None) -> OpenSCADObject:
1146+
scale_factors:Sequence[Union[Vector2, float]]=None,
1147+
connect_ends = False) -> OpenSCADObject:
11471148
# Extrude the convex curve defined by shape_pts along path_pts.
11481149
# -- For predictable results, shape_pts must be planar, convex, and lie
11491150
# in the XY plane centered around the origin.
@@ -1161,72 +1162,91 @@ def extrude_along_path( shape_pts:Points,
11611162

11621163
src_up = Vector3(*UP_VEC)
11631164

1165+
shape_pt_count = len(shape_pts)
1166+
1167+
tangent_path_points: List[Point3] = []
1168+
if connect_ends:
1169+
tangent_path_points = [path_pts[-1]] + path_pts + [path_pts[0]]
1170+
else:
1171+
first = Point3(*(path_pts[0] - (path_pts[1] - path_pts[0])))
1172+
last = Point3(*(path_pts[-1] - (path_pts[-2] - path_pts[-1])))
1173+
tangent_path_points = [first] + path_pts + [last]
1174+
tangents = [tangent_path_points[i+2] - tangent_path_points[i] for i in range(len(path_pts))]
1175+
11641176
for which_loop in range(len(path_pts)):
11651177
path_pt = path_pts[which_loop]
1178+
tangent = tangents[which_loop]
1179+
scale_factor = scale_factors[which_loop] if scale_factors else 1
1180+
this_loop = shape_pts[:]
1181+
this_loop = _scale_loop(this_loop, scale_factor)
1182+
this_loop = transform_to_point(this_loop, dest_point=path_pt, dest_normal=tangent, src_up=src_up)
1183+
loop_start_index = which_loop * shape_pt_count
1184+
1185+
if (which_loop < len(path_pts) - 1):
1186+
loop_facets = _loop_facet_indices(loop_start_index, shape_pt_count)
1187+
facet_indices += loop_facets
1188+
1189+
# Add the transformed points & facets to our final list
1190+
polyhedron_pts += this_loop
11661191

1167-
# calculate the tangent to the curve at this point
1168-
if which_loop > 0 and which_loop < len(path_pts) - 1:
1169-
prev_pt = path_pts[which_loop - 1]
1170-
next_pt = path_pts[which_loop + 1]
1171-
1172-
v_prev = path_pt - prev_pt
1173-
v_next = next_pt - path_pt
1174-
tangent = v_prev + v_next
1175-
elif which_loop == 0:
1176-
tangent = path_pts[which_loop + 1] - path_pt
1177-
elif which_loop == len(path_pts) - 1:
1178-
tangent = path_pt - path_pts[which_loop - 1]
1179-
1180-
# Scale points
1181-
this_loop = shape_pts[:] # type: ignore
1182-
scale_x, scale_y = [1, 1]
1183-
if scale_factors:
1184-
scale = scale_factors[which_loop]
1185-
if isinstance(scale, (int, float)):
1186-
scale_x, scale_y = scale, scale
1187-
elif isinstance(scale, Vector2):
1188-
scale_x, scale_y = scale.x, scale.y
1189-
else:
1190-
raise ValueError(f'Unable to scale shape_pts with scale value: {scale}')
1191-
this_loop = [Point3(v.x * scale_x, v.y * scale_y, v.z) for v in this_loop]
1192+
if connect_ends:
1193+
next_loop_start_index = len(polyhedron_pts) - shape_pt_count
1194+
loop_facets = _loop_facet_indices(0, shape_pt_count, next_loop_start_index)
1195+
facet_indices += loop_facets
11921196

1193-
# Rotate & translate
1194-
this_loop = transform_to_point(this_loop, dest_point=path_pt,
1195-
dest_normal=tangent, src_up=src_up)
1197+
else:
1198+
# endcaps at start & end of extrusion
1199+
# NOTE: this block adds points & indices to the polyhedron, so it's
1200+
# very sensitive to the order this is happening in
1201+
start_cap_index = len(polyhedron_pts)
1202+
end_cap_index = start_cap_index + 1
1203+
last_loop_start_index = len(polyhedron_pts) - shape_pt_count
11961204

1197-
# Add the transformed points to our final list
1198-
polyhedron_pts += this_loop
1199-
# And calculate the facet indices
1200-
shape_pt_count = len(shape_pts)
1201-
segment_start = which_loop * shape_pt_count
1202-
segment_end = segment_start + shape_pt_count - 1
1203-
if which_loop < len(path_pts) - 1:
1204-
for i in range(segment_start, segment_end):
1205-
facet_indices.append( (i, i + shape_pt_count, i + 1) )
1206-
facet_indices.append( (i + 1, i + shape_pt_count, i + shape_pt_count + 1) )
1207-
facet_indices.append( (segment_start, segment_end, segment_end + shape_pt_count) )
1208-
facet_indices.append( (segment_start, segment_end + shape_pt_count, segment_start + shape_pt_count) )
1209-
1210-
# endcap at start of extrusion
1211-
start_cap_index = len(polyhedron_pts)
1212-
start_loop_pts = polyhedron_pts[:shape_pt_count]
1213-
start_loop_indices = list(range(shape_pt_count))
1214-
start_centroid, start_facet_indices = end_cap(start_cap_index, start_loop_pts, start_loop_indices)
1215-
polyhedron_pts.append(start_centroid)
1216-
facet_indices += start_facet_indices
1217-
1218-
# endcap at end of extrusion
1219-
end_cap_index = len(polyhedron_pts)
1220-
last_loop_start_index = len(polyhedron_pts) - shape_pt_count - 1
1221-
end_loop_pts = polyhedron_pts[last_loop_start_index:-1]
1222-
end_loop_indices = list(range(last_loop_start_index, len(polyhedron_pts) - 1))
1223-
end_centroid, end_facet_indices = end_cap(end_cap_index, end_loop_pts, end_loop_indices)
1224-
polyhedron_pts.append(end_centroid)
1225-
facet_indices += end_facet_indices
1205+
start_loop_pts = polyhedron_pts[:shape_pt_count]
1206+
end_loop_pts = polyhedron_pts[last_loop_start_index:]
1207+
1208+
start_loop_indices = list(range(0, shape_pt_count))
1209+
end_loop_indices = list(range(last_loop_start_index, last_loop_start_index + shape_pt_count))
1210+
1211+
start_centroid, start_facet_indices = _end_cap(start_cap_index, start_loop_pts, start_loop_indices)
1212+
end_centroid, end_facet_indices = _end_cap(end_cap_index, end_loop_pts, end_loop_indices)
1213+
polyhedron_pts += [start_centroid, end_centroid]
1214+
facet_indices += start_facet_indices
1215+
facet_indices += end_facet_indices
12261216

12271217
return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore
12281218

1229-
def end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]:
1219+
def _loop_facet_indices(loop_start_index:int, loop_pt_count:int, next_loop_start_index=None) -> List[FacetIndices]:
1220+
facet_indices: List[FacetIndices] = []
1221+
# nlsi == next_loop_start_index
1222+
if next_loop_start_index == None:
1223+
next_loop_start_index = loop_start_index + loop_pt_count
1224+
loop_indices = list(range(loop_start_index, loop_pt_count + loop_start_index)) + [loop_start_index]
1225+
next_loop_indices = list(range(next_loop_start_index, loop_pt_count + next_loop_start_index )) + [next_loop_start_index]
1226+
1227+
for i, (a, b) in enumerate(zip(loop_indices[:-1], loop_indices[1:])):
1228+
# c--d
1229+
# |\ |
1230+
# | \|
1231+
# a--b
1232+
c, d = next_loop_indices[i: i+2]
1233+
facet_indices.append((a,c,b))
1234+
facet_indices.append((b,c,d))
1235+
return facet_indices
1236+
1237+
def _scale_loop(points:Sequence[Point3], scale_factor:Union[float, Point2]=None) -> List[Point3]:
1238+
scale_x, scale_y = [1, 1]
1239+
if scale_factor:
1240+
if isinstance(scale_factor, (int, float)):
1241+
scale_x, scale_y = scale_factor, scale_factor
1242+
elif isinstance(scale_factor, Vector2):
1243+
scale_x, scale_y = scale_factor.x, scale_factor.y
1244+
else:
1245+
raise ValueError(f'Unable to scale shape_pts with scale_factor: {scale_factor}')
1246+
this_loop = [Point3(v.x * scale_x, v.y * scale_y, v.z) for v in points]
1247+
return this_loop
1248+
1249+
def _end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]:
12301250
# Assume points are a basically planar, basically convex polygon with polyhedron
12311251
# indices `vertex_indices`.
12321252
# Return a new point that is the centroid of the polygon and a list of

0 commit comments

Comments
 (0)