Extrude Path

sweep a profile along a 3D centerline.

▶ Run in browser

Tags: 3d

Demonstrates:

  • Mesh.extrude_path() generating a tube mesh from a polyline + circle profile

  • sides / radius overriding the default 8-sided profile (the ribbon uses 12)

  • closed=True joining the ends of a looping centerline, so the figure-eight has no seam gap

  • A 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())