Extrude Path¶
sweep a profile along a 3D centerline.
▶ Run in browserTags: 3d
Demonstrates:
Mesh.extrude_path() generating a tube mesh from a polyline + circle profile
sides/radiusoverriding the default 8-sided profile (the ribbon uses 12)closed=Truejoining the ends of a looping centerline, so the figure-eight has no seam gapA custom square cross-section (
profile) swept along a curved rail
Controls: Escape - Quit
Run: uv run python examples/features/3d/extrude_path.py
Source¶
1"""Extrude Path: sweep a profile along a 3D centerline.
2
3Demonstrates:
4 - Mesh.extrude_path() generating a tube mesh from a polyline + circle profile
5 - ``sides`` / ``radius`` overriding the default 8-sided profile (the ribbon uses 12)
6 - ``closed=True`` joining the ends of a looping centerline, so the figure-eight
7 has no seam gap
8 - A custom square cross-section (``profile``) swept along a curved rail
9
10Controls:
11 Escape - Quit
12
13Run: uv run python examples/features/3d/extrude_path.py
14"""
15
16from __future__ import annotations
17
18import math
19
20import numpy as np
21
22from simvx.core import (
23 Camera3D,
24 DirectionalLight3D,
25 Input,
26 Key,
27 Material,
28 Mesh,
29 MeshInstance3D,
30 Node,
31 Text2D,
32 Vec3,
33 WorldEnvironment,
34)
35from simvx.graphics import App
36
37
38def _figure_eight(n: int = 80, scale: float = 4.0) -> list:
39 """Lissajous-style figure-eight centerline lying in the XZ plane."""
40 pts = []
41 for i in range(n):
42 t = i / n * math.tau
43 x = math.sin(t * 2.0) * scale
44 z = math.sin(t) * scale
45 y = math.sin(t * 4.0) * 0.4 # gentle vertical wobble
46 pts.append((x, y, z))
47 return pts
48
49
50def _spline_arc(n: int = 30) -> list:
51 """Quarter-circle arc: used for the square-profile rail."""
52 return [(math.cos(t) * 3.5, 0.0, math.sin(t) * 3.5) for t in np.linspace(0.0, math.pi * 0.5, n)]
53
54
55class ExtrudePathScene(Node):
56 input_actions = {"quit": [Key.ESCAPE]}
57
58 def on_ready(self):
59 env = self.add_child(WorldEnvironment())
60 env.bloom_enabled = False
61 env.ambient_light_energy = 0.45
62
63 sun = DirectionalLight3D(position=(6, 10, 4))
64 sun.intensity = 1.1
65 sun.look_at(Vec3(0, 0, 0))
66 self.add_child(sun)
67
68 self.add_child(
69 Camera3D(
70 position=(8, 6, 8),
71 fov=60.0,
72 near=0.1,
73 far=200.0,
74 look_at=Vec3(0, 0.5, 0),
75 )
76 )
77
78 # Circle profile sweep around a closed loop. The centerline stops one
79 # segment short of its start point, so ``closed=True`` is what joins the
80 # ends into a continuous ribbon.
81 ribbon_mesh = Mesh.extrude_path(_figure_eight(), sides=12, radius=0.18, closed=True)
82 self.add_child(
83 MeshInstance3D(
84 mesh=ribbon_mesh,
85 material=Material(colour=(0.9, 0.4, 0.2, 1.0), roughness=0.4, metallic=0.0),
86 position=(0, 0.6, 0),
87 )
88 )
89
90 # Square-profile rail along a quarter arc.
91 square = np.array([[0.3, 0.05], [-0.3, 0.05], [-0.3, -0.05], [0.3, -0.05]], dtype=np.float32)
92 rail = Mesh.extrude_path(_spline_arc(), profile=square)
93 self.add_child(
94 MeshInstance3D(
95 mesh=rail,
96 material=Material(colour=(0.6, 0.6, 0.7, 1.0), roughness=0.2, metallic=0.9),
97 position=(-3.5, 0.0, -3.5),
98 )
99 )
100
101 # Ground plane.
102 self.add_child(
103 MeshInstance3D(
104 mesh=Mesh.cube(size=1.0),
105 material=Material(colour=(0.18, 0.2, 0.22, 1.0)),
106 position=(0, -0.1, 0),
107 scale=Vec3(40, 0.1, 40),
108 )
109 )
110
111 self.add_child(
112 Text2D(
113 text="Mesh.extrude_path: figure-eight (circle profile) + arc (square profile)",
114 position=(12, 12),
115 font_scale=1.0,
116 )
117 )
118
119 def on_update(self, dt: float):
120 if Input.is_action_just_pressed("quit"):
121 self.app.quit()
122
123
124if __name__ == "__main__":
125 App(title="Mesh.extrude_path", width=1280, height=720).run(ExtrudePathScene())