Skip to content

Commit b388e44

Browse files
committed
Improved end cap algorithm on solid.utils.extrude_along_path(). It can now accept somewhat concave polygon (e.g., stars) profiles and still return a valid non-intersecting triangulation of a polygon.
1 parent a045f82 commit b388e44

File tree

2 files changed

+60
-24
lines changed

2 files changed

+60
-24
lines changed

solid/examples/path_extrude_example.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,10 @@ def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]:
4343
return points
4444

4545
def extrude_example():
46-
# Note the incorrect triangulation at the two ends of the path. This
47-
# is because star isn't convex, and the triangulation algorithm for
48-
# the two end caps only works for convex shapes.
4946
path_rad = 50
5047
shape = star(num_points=5)
5148
path = sinusoidal_ring(rad=path_rad, segments=240)
5249

53-
# # If scale_factors aren't included, they'll default to
54-
# # no scaling at each step along path. Here, let's
55-
# # make the shape twice as big at beginning and end of the path
56-
# scales = [1] * len(path)
57-
# n = len(path)
58-
# scales = [1 + 0.5*sin(i*6*pi/n) for i in range(n)]
59-
# scales[0] = 2
60-
# scales[-1] = 2
61-
6250
extruded = extrude_along_path( shape_pts=shape, path_pts=path)
6351
# Label
6452
extruded += translate([-path_rad/2, 2*path_rad])(text('Basic Extrude'))
@@ -73,7 +61,6 @@ def extrude_example_xy_scaling() -> OpenSCADObject:
7361
# angle: from 0 to 6*Pi
7462
angles = list((i/(num_points - 1)*tau*3 for i in range(len(path))))
7563

76-
7764
# If scale_factors aren't included, they'll default to
7865
# no scaling at each step along path.
7966
no_scale_obj = translate([-path_rad / 2, 2 * path_rad])(text('No Scale'))

solid/utils.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
from typing import Any, Union, Tuple, Sequence, List, Optional, Callable, Dict, cast
2121
Point23 = Union[Point2, Point3]
2222
Vector23 = Union[Vector2, Vector3]
23+
PointVec23 = Union[Point2, Point3, Vector2, Vector3]
2324
Line23 = Union[Line2, Line3]
2425
LineSegment23 = Union[LineSegment2, LineSegment3]
2526

27+
FacetIndices = Tuple[int, int, int]
28+
2629
Tuple2 = Tuple[float, float]
2730
Tuple3 = Tuple[float, float, float]
2831
EucOrTuple = Union[Point3,
@@ -107,7 +110,6 @@ def grid_plane(grid_unit:int=12, count:int=10, line_weight:float=0.1, plane:str=
107110

108111
return t
109112

110-
111113
def distribute_in_grid(objects:Sequence[OpenSCADObject],
112114
max_bounding_box:Tuple[float,float],
113115
rows_and_cols: Tuple[int,int]=None) -> OpenSCADObject:
@@ -814,6 +816,27 @@ def scad_matrix(euclid_matrix4):
814816
[a.m, a.n, a.o, a.p]
815817
]
816818

819+
def centroid(points:Sequence[PointVec23]) -> PointVec23:
820+
if not points:
821+
raise ValueError(f"centroid(): argument `points` is empty")
822+
first = points[0]
823+
is_3d = isinstance(first, (Vector3, Point3))
824+
if is_3d:
825+
total = Vector3(0,0,0)
826+
else:
827+
total = Vector2(0, 0)
828+
829+
for p in points:
830+
total += p
831+
total /= len(points)
832+
833+
if isinstance(first, Point2):
834+
return Point2(*total)
835+
elif isinstance(first, Point3):
836+
return Point3(*total)
837+
else:
838+
return total
839+
817840
# ==============
818841
# = Transforms =
819842
# ==============
@@ -1184,19 +1207,45 @@ def extrude_along_path( shape_pts:Points,
11841207
facet_indices.append( (segment_start, segment_end, segment_end + shape_pt_count) )
11851208
facet_indices.append( (segment_start, segment_end + shape_pt_count, segment_start + shape_pt_count) )
11861209

1187-
# Cap the start of the polyhedron
1188-
for i in range(1, shape_pt_count - 1):
1189-
facet_indices.append((0, i, i + 1))
1190-
1191-
# And the end (could be rolled into the earlier loop)
1192-
# FIXME: concave cross-sections will cause this end-capping algorithm
1193-
# to fail
1194-
end_cap_base = len(polyhedron_pts) - shape_pt_count
1195-
for i in range(end_cap_base + 1, len(polyhedron_pts) - 1):
1196-
facet_indices.append( (end_cap_base, i + 1, i) )
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
11971226

11981227
return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore
11991228

1229+
def end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]:
1230+
# Assume points are a basically planar, basically convex polygon with polyhedron
1231+
# indices `vertex_indices`.
1232+
# Return a new point that is the centroid of the polygon and a list of
1233+
# vertex triangle indices that covers the whole polygon.
1234+
# (We can actually accept relatively non-planar and non-convex polygons,
1235+
# but not anything pathological. Stars are fine, internal pockets would
1236+
# cause incorrect faceting)
1237+
1238+
# NOTE: In order to deal with moderately-concave polygons, we add a point
1239+
# to the center of the end cap. This will have a new index that we require
1240+
# as an argument.
1241+
1242+
new_point = centroid(points)
1243+
new_facets = []
1244+
second_indices = vertex_indices[1:] + [vertex_indices[0]]
1245+
new_facets = [(new_point_index, a, b) for a, b in zip(vertex_indices, second_indices)]
1246+
1247+
return (new_point, new_facets)
1248+
12001249
def frange(*args):
12011250
"""
12021251
# {{{ http://code.activestate.com/recipes/577068/ (r1)

0 commit comments

Comments
 (0)