Extrude Path¶
sweep a profile along a 3D centerline.
▶ Run in browserTags: 3d
Mesh.extrude_path builds a tube or ribbon by sweeping a 2D cross-section along
a polyline of 3D points: the primitive behind race tracks, rivers, rails, cables
and tentacles. Pass the centerline as a list of (x, y, z) points (or an
(N, 3) array) and hang the result on a MeshInstance3D like any other mesh:
tube = Mesh.extrude_path(points, sides=12, radius=0.18, closed=True)
With no profile the cross-section is a regular polygon of sides corners
(8 by default) and the given radius. closed=True joins the last point back
to the first, so a looping centerline gets no gap at the seam. To sweep any other
shape, pass profile, an (M, 2) array of points in the cross-section’s own
plane, with X across the path and Y along the path’s local up; sides and
radius are then ignored.
The orange figure-eight is a 12-sided tube around a closed loop whose points stop
one step short of the start, so closed=True is what joins it. The metal rail
sweeps a flat rectangle along a quarter circle.
Source¶
1"""Extrude Path: sweep a profile along a 3D centerline.
2
3``Mesh.extrude_path`` builds a tube or ribbon by sweeping a 2D cross-section along
4a polyline of 3D points: the primitive behind race tracks, rivers, rails, cables
5and tentacles. Pass the centerline as a list of ``(x, y, z)`` points (or an
6``(N, 3)`` array) and hang the result on a ``MeshInstance3D`` like any other mesh:
7
8 tube = Mesh.extrude_path(points, sides=12, radius=0.18, closed=True)
9
10With no ``profile`` the cross-section is a regular polygon of ``sides`` corners
11(8 by default) and the given ``radius``. ``closed=True`` joins the last point back
12to the first, so a looping centerline gets no gap at the seam. To sweep any other
13shape, pass ``profile``, an ``(M, 2)`` array of points in the cross-section's own
14plane, with X across the path and Y along the path's local up; ``sides`` and
15``radius`` are then ignored.
16
17The orange figure-eight is a 12-sided tube around a closed loop whose points stop
18one step short of the start, so ``closed=True`` is what joins it. The metal rail
19sweeps a flat rectangle along a quarter circle.
20"""
21
22import math
23
24import numpy as np
25
26from simvx.core import (
27 Anchor,
28 Camera3D,
29 CanvasLayer,
30 DirectionalLight3D,
31 Label,
32 Material,
33 Mesh,
34 MeshInstance3D,
35 Node,
36 WorldEnvironment,
37)
38from simvx.graphics import App
39
40
41def figure_eight(n: int = 80, scale: float = 4.0) -> list[tuple[float, float, float]]:
42 """A figure-eight in the XZ plane with a gentle vertical wobble, open at the seam."""
43 ts = (i / n * math.tau for i in range(n))
44 return [(math.sin(2 * t) * scale, math.sin(4 * t) * 0.4, math.sin(t) * scale) for t in ts]
45
46
47def quarter_arc(n: int = 30, radius: float = 3.5) -> list[tuple[float, float, float]]:
48 return [(math.cos(t) * radius, 0.0, math.sin(t) * radius) for t in np.linspace(0.0, math.pi / 2, n)]
49
50
51# A flat 0.6 x 0.1 rectangle, the rail's cross-section.
52RAIL_PROFILE = np.array([[0.3, 0.05], [-0.3, 0.05], [-0.3, -0.05], [0.3, -0.05]], dtype=np.float32)
53
54
55class ExtrudePathScene(Node):
56 def on_ready(self):
57 self.add_child(WorldEnvironment(ambient_light_energy=0.45))
58 self.add_child(DirectionalLight3D(direction=(-6, -10, -4), intensity=1.1))
59 self.add_child(Camera3D(position=(8, 6, 8), look_at=(0, 0.5, 0)))
60
61 ribbon = Mesh.extrude_path(figure_eight(), sides=12, radius=0.18, closed=True)
62 orange = Material(colour=(0.9, 0.4, 0.2), roughness=0.4)
63 self.add_child(MeshInstance3D(mesh=ribbon, material=orange, position=(0, 0.6, 0)))
64
65 rail = Mesh.extrude_path(quarter_arc(), profile=RAIL_PROFILE)
66 metal = Material(colour=(0.6, 0.6, 0.7), roughness=0.2, metallic=0.9)
67 self.add_child(MeshInstance3D(mesh=rail, material=metal, position=(-3.5, 0.05, -3.5)))
68
69 ground = Material(colour=(0.18, 0.2, 0.22))
70 self.add_child(MeshInstance3D(mesh=Mesh.plane(40.0), material=ground))
71
72 hud = self.add_child(CanvasLayer())
73 caption = "Mesh.extrude_path: figure-eight (circle profile) + arc (rectangle profile)"
74 hud.add_child(Label(caption, anchor=Anchor.TOP_LEFT, inset=12))
75
76
77if __name__ == "__main__":
78 App(title="Extrude Path", width=1280, height=720).run(ExtrudePathScene())