Skip to content

Commit 29618eb

Browse files
committed
-- Separated out solid.utils.extrude_along_path() into its own file.
-- Added custom scale, rotation, and arbitrary transform arguments -- Extended examples to illustrate new behaviors -- Testing
1 parent e994edd commit 29618eb

File tree

6 files changed

+528
-236
lines changed

6 files changed

+528
-236
lines changed

README.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,11 @@ rounds.
380380
Extrude Along Path
381381
------------------
382382

383-
``solid.utils.extrude_along_path(shape_pts, path_pts, scale_factors=None)``
383+
``solid.utils.extrude_along_path()`` is quite powerful. It can do everything that
384+
OpenSCAD's ``linear_extrude() `` and ``rotate_extrude()`` can do, and lots, lots more.
385+
Scale to custom values throughout the extrusion. Rotate smoothly through the entire
386+
extrusion or specify particular rotations for each step. Apply arbitrary transform
387+
functions to every point in the extrusion.
384388

385389
See
386390
`solid/examples/path_extrude_example.py <https://github.com/SolidCode/SolidPython/blob/master/solid/examples/path_extrude_example.py>`__
Lines changed: 145 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,60 @@
11
#! /usr/bin/env python3
2+
from solid.objects import linear_extrude
23
from solid.solidpython import OpenSCADObject
34
import sys
45
from math import cos, radians, sin, pi, tau
56
from pathlib import Path
67

7-
from euclid3 import Point2, Point3
8+
from euclid3 import Point2, Point3, Vector3
89

9-
from solid import scad_render_to_file, text, translate
10-
from solid.utils import extrude_along_path, right
10+
from solid import scad_render_to_file, text, translate, cube, color, rotate
11+
from solid.utils import UP_VEC, Vector23, distribute_in_grid, extrude_along_path
12+
from solid.utils import down, right, frange, lerp
1113

1214

13-
from typing import Set, Sequence, List, Callable, Optional, Union, Iterable
15+
from typing import Set, Sequence, List, Callable, Optional, Union, Iterable, Tuple
1416

1517
SEGMENTS = 48
18+
PATH_RAD = 50
19+
SHAPE_RAD = 15
20+
21+
TEXT_LOC = [-0.6 *PATH_RAD, 1.6 * PATH_RAD]
1622

1723
def basic_extrude_example():
18-
path_rad = 50
24+
path_rad = PATH_RAD
1925
shape = star(num_points=5)
2026
path = sinusoidal_ring(rad=path_rad, segments=240)
2127

28+
# At its simplest, just sweep a shape along a path
2229
extruded = extrude_along_path( shape_pts=shape, path_pts=path)
23-
# Label
24-
extruded += translate([-path_rad/2, 2*path_rad])(text('Basic Extrude'))
30+
extruded += make_label('Basic Extrude')
2531
return extruded
2632

2733
def extrude_example_xy_scaling() -> OpenSCADObject:
2834
num_points = SEGMENTS
29-
path_rad = 50
35+
path_rad = PATH_RAD
3036
circle = circle_points(15)
3137
path = circle_points(rad = path_rad)
3238

33-
# angle: from 0 to 6*Pi
34-
angles = list((i/(num_points - 1)*tau*3 for i in range(len(path))))
35-
36-
# If scale_factors aren't included, they'll default to
39+
# If scales aren't included, they'll default to
3740
# no scaling at each step along path.
38-
no_scale_obj = translate([-path_rad / 2, 2 * path_rad])(text('No Scale'))
41+
no_scale_obj = make_label('No Scale')
3942
no_scale_obj += extrude_along_path(circle, path)
4043

44+
# angles: from 0 to 6*Pi
45+
angles = list((frange(0, 3*tau, num_steps=len(path))))
46+
4147
# With a 1-D scale factor, an extrusion grows and shrinks uniformly
4248
x_scales = [(1 + cos(a)/2) for a in angles]
43-
x_obj = translate([-path_rad / 2, 2 * path_rad])(text('1D Scale'))
44-
x_obj += extrude_along_path(circle, path, scale_factors=x_scales)
49+
x_obj = make_label('1D Scale')
50+
x_obj += extrude_along_path(circle, path, scales=x_scales)
4551

4652
# With a 2D scale factor, a shape's X & Y dimensions can scale
4753
# independently, leading to more interesting shapes
4854
# X & Y scales vary between 0.5 & 1.5
4955
xy_scales = [Point2( 1 + cos(a)/2, 1 + sin(a)/2) for a in angles]
50-
xy_obj = translate([-path_rad / 2, 2 * path_rad])( text('2D Scale'))
51-
xy_obj += extrude_along_path(circle, path, scale_factors=xy_scales)
56+
xy_obj = make_label('2D Scale')
57+
xy_obj += extrude_along_path(circle, path, scales=xy_scales)
5258

5359
obj = no_scale_obj + right(3*path_rad)(x_obj) + right(6 * path_rad)(xy_obj)
5460
return obj
@@ -57,20 +63,117 @@ def extrude_example_capped_ends() -> OpenSCADObject:
5763
num_points = SEGMENTS/2
5864
path_rad = 50
5965
circle = star(6)
60-
path = circle_points(rad = path_rad)
66+
path = circle_points(rad = path_rad)[:-4]
6167

6268
# If `connect_ends` is False or unspecified, ends will be capped.
6369
# 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)
70+
capped_obj = make_label('Capped Ends')
71+
capped_obj += extrude_along_path(circle, path, connect_ends=False, cap_ends=True)
6672

6773
# If `connect_ends` is specified, create a continuous manifold object
68-
connected_obj = translate([-path_rad / 2, 2 * path_rad])(text('Connected Ends'))
74+
connected_obj = make_label('Connected Ends')
6975
connected_obj += extrude_along_path(circle, path, connect_ends=True)
7076

7177
return capped_obj + right(3*path_rad)(connected_obj)
7278

73-
def sinusoidal_ring(rad=25, segments=SEGMENTS):
79+
def extrude_example_rotations() -> OpenSCADObject:
80+
path_rad = PATH_RAD
81+
shape = star(num_points=5)
82+
path = circle_points(path_rad, num_points=240)
83+
84+
# For a simple example, make one complete revolution by the end of the extrusion
85+
simple_rot = make_label('Simple Rotation')
86+
simple_rot += extrude_along_path(shape, path, rotations=[360], connect_ends=True)
87+
88+
# For a more complex set of rotations, add a rotation degree for each point in path
89+
complex_rotations = []
90+
degs = 0
91+
oscillation_max = 60
92+
93+
for i in frange(0, 1, num_steps=len(path)):
94+
# For the first third of the path, do one complete rotation
95+
if i <= 0.333:
96+
degs = i/0.333*360
97+
# For the second third of the path, oscillate between +/- oscillation_max degrees
98+
elif i <= 0.666:
99+
angle = lerp(i, 0.333, 0.666, 0, 2*tau)
100+
degs = oscillation_max * sin(angle)
101+
# For the last third of the path, oscillate increasingly fast but with smaller magnitude
102+
else:
103+
# angle increases in a nonlinear curve, so
104+
# oscillations should get quicker and quicker
105+
x = lerp(i, 0.666, 1.0, 0, 2)
106+
angle = pow(x, 2.2) * tau
107+
# decrease the size of the oscillations by a factor of 10
108+
# over the course of this stretch
109+
osc = lerp(i, 0.666, 1.0, oscillation_max, oscillation_max/10)
110+
degs = osc * sin(angle)
111+
complex_rotations.append(degs)
112+
113+
complex_rot = make_label('Complex Rotation')
114+
complex_rot += extrude_along_path(shape, path, rotations=complex_rotations)
115+
116+
# Make some red markers to show the boundaries between the three sections of this path
117+
marker_w = SHAPE_RAD * 1.5
118+
marker = translate([path_rad, 0, 0])(
119+
cube([marker_w, 1, marker_w], center=True)
120+
)
121+
markers = [color('red')(rotate([0,0,120*i])(marker)) for i in range(3)]
122+
complex_rot += markers
123+
124+
return simple_rot + right(3*path_rad)(complex_rot)
125+
126+
def extrude_example_transforms() -> OpenSCADObject:
127+
path_rad = PATH_RAD
128+
height = 2*SHAPE_RAD
129+
num_steps = 120
130+
131+
shape = circle_points(rad=path_rad, num_points=120)
132+
path = [Point3(0,0,i) for i in frange(0, height, num_steps=num_steps)]
133+
134+
max_rotation = radians(15)
135+
max_z_displacement = height/10
136+
up = Vector3(0,0,1)
137+
138+
# The transforms argument is powerful.
139+
# Each point in the entire extrusion will call this function with unique arguments:
140+
# -- `path_norm` in [0, 1] specifying how far along in the extrusion a point's loop is
141+
# -- `loop_norm` in [0, 1] specifying where in its loop a point is.
142+
def point_trans(point: Point3, path_norm:float, loop_norm: float) -> Point3:
143+
# scale the point from 1x to 2x in the course of the
144+
# extrusion,
145+
scale = 1 + path_norm*path_norm/2
146+
p = scale * point
147+
148+
# Rotate the points sinusoidally up to max_rotation
149+
p = p.rotate_around(up, max_rotation*sin(tau*path_norm))
150+
151+
152+
# Oscillate z values sinusoidally, growing from
153+
# 0 magnitude to max_z_displacement
154+
max_z = lerp(path_norm, 0, 1, 0, max_z_displacement)
155+
angle = lerp(loop_norm, 0, 1, 0, 10*tau)
156+
p.z += max_z*sin(angle)
157+
return p
158+
159+
no_trans = make_label('No Transform')
160+
no_trans += down(height/2)(
161+
extrude_along_path(shape, path, cap_ends=False)
162+
)
163+
164+
# We can pass transforms a single function that will be called on all points,
165+
# or pass a list with a transform function for each point along path
166+
arb_trans = make_label('Arbitrary Transform')
167+
arb_trans += down(height/2)(
168+
extrude_along_path(shape, path, transforms=[point_trans], cap_ends=False)
169+
)
170+
171+
return no_trans + right(3*path_rad)(arb_trans)
172+
173+
# ============
174+
# = GEOMETRY =
175+
# ============
176+
def sinusoidal_ring(rad=25, segments=SEGMENTS) -> List[Point3]:
74177
outline = []
75178
for i in range(segments):
76179
angle = radians(i * 360 / segments)
@@ -83,26 +186,42 @@ def sinusoidal_ring(rad=25, segments=SEGMENTS):
83186
outline.append(Point3(x, y, z))
84187
return outline
85188

86-
def star(num_points=5, outer_rad=15, dip_factor=0.5):
189+
def star(num_points=5, outer_rad=SHAPE_RAD, dip_factor=0.5) -> List[Point3]:
87190
star_pts = []
88191
for i in range(2 * num_points):
89192
rad = outer_rad - i % 2 * dip_factor * outer_rad
90193
angle = radians(360 / (2 * num_points) * i)
91194
star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0))
92195
return star_pts
93196

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)]
197+
def circle_points(rad: float = SHAPE_RAD, num_points: int = SEGMENTS) -> List[Point2]:
198+
angles = frange(0, tau, num_steps=num_points, include_end=True)
96199
points = list([Point2(rad*cos(a), rad*sin(a)) for a in angles])
97200
return points
98201

202+
def make_label(message:str, text_loc:Tuple[float, float]=TEXT_LOC, height=5) -> OpenSCADObject:
203+
return translate(text_loc)(
204+
linear_extrude(height)(
205+
text(message)
206+
)
207+
)
208+
209+
# ===============
210+
# = ENTRY POINT =
211+
# ===============
99212
if __name__ == "__main__":
100213
out_dir = sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent
101214

102215
basic_extrude = basic_extrude_example()
103216
scaled_extrusions = extrude_example_xy_scaling()
104217
capped_extrusions = extrude_example_capped_ends()
105-
a = basic_extrude + translate([0,-250])(scaled_extrusions) + translate([0, -500])(capped_extrusions)
218+
rotated_extrusions = extrude_example_rotations()
219+
arbitrary_transforms = extrude_example_transforms()
220+
all_objs = [basic_extrude, scaled_extrusions, capped_extrusions, rotated_extrusions, arbitrary_transforms]
221+
222+
a = distribute_in_grid(all_objs,
223+
max_bounding_box=[4*PATH_RAD, 4*PATH_RAD],
224+
rows_and_cols=[len(all_objs), 1])
106225

107226
file_out = scad_render_to_file(a, out_dir=out_dir, include_orig_code=True)
108227
print(f"{__file__}: SCAD file written to: \n{file_out}")

0 commit comments

Comments
 (0)