Examples
This page explains each script in examples/ and lets you expand the full source.
cli_export.py
Command-line example that parses arguments, builds a mesh, and exports it with Trimesh.
Source
import argparse
from trimesh.creation import cylinder
def create_mesh(radius: float = 10.0, height: float = 20.0):
return cylinder(radius=radius, height=height)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--radius", type=float, default=10.0)
parser.add_argument("--height", type=float, default=20.0)
parser.add_argument("--out", required=True, help="Output file, e.g. model.stl")
args = parser.parse_args()
mesh = create_mesh(radius=args.radius, height=args.height)
mesh.export(args.out)
if __name__ == "__main__":
main()

colors.py
Builds a list of colored text meshes, one per Color enum value. This shows how returning a list[Trimesh] enables debug-mode display of multiple meshes at once.
Source
from scadview import Color, set_mesh_color, text
def create_mesh():
all_meshes = []
for i, color in enumerate(Color):
mesh = set_mesh_color(text(color.name), color, 1.0).apply_translation(
[0, (len(Color) - i) * 15, 0]
)
all_meshes.append(mesh)
return all_meshes

cube_minus_sphere.py
Creates boxes and spheres with translucent colors and returns a list of intermediate shapes. It demonstrates boolean experimentation (difference is commented) and using colors to inspect overlaps.
Source
from trimesh.creation import box, icosphere
from scadview import Color, set_mesh_color
def create_mesh():
scale = 100.0
box_mesh = set_mesh_color(
box([scale, scale, scale]).subdivide(), Color.MAGENTA, 0.5
)
box_mesh2 = set_mesh_color(box([scale, scale, scale]).subdivide(), Color.GREEN, 0.5)
sphere_mesh = set_mesh_color(
icosphere(radius=0.4 * scale, subdivisions=3), Color.YELLOW, 0.5
)
sphere_mesh2 = set_mesh_color(
icosphere(radius=0.6 * scale, subdivisions=3), Color.BEIGE, 0.5
)
return [
box_mesh,
sphere_mesh2.apply_translation([scale / 2, 0, 0]),
sphere_mesh,
box_mesh2.apply_translation([scale / 2, scale / 2, scale / 2]),
]

invalid_code.py
Imports a non-existent module to force an import error. Useful for verifying error handling.
Source
import non_existent_module # noqa: F401

invalid_code_2.py
Calls an undefined function (prin) to trigger a runtime error. Useful for testing error reporting in the UI.
Source
def create_mesh():
prin() # noqa F821

koch_snowflake_vase.py
Generates a Koch-snowflake outline, then uses linear_extrude with twist, slices, and scale to create a tall, twisted vase. Demonstrates procedural 2D shape refinement and extrusion.
Source
import numpy as np
import shapely.geometry as sg
from scadview import linear_extrude
KOCH_DEFAULT_BUMP_LENGTH_FRACTION = np.sqrt(
1.0 / 3.0**2 - 1.0 / 6.0**2
) # 1/3 edge length, 1/6 from midpoint
# Bump length multiplier for the Koch snowflake
# Less that 0.58 does not create a bump, but a straight edge
# More than 1.75-ish creates intersecting edges
BUMP_LENGTH_FRACTION_MULTIPLIER = 1.0
ORDER = 4
R = 50
HEIGHT = 150
TWIST = 60
TRIANGLE_VERTEX_COUNT = 3
SLICES = 20
SCALE = (1.1, 1.5) # scalar or (sx, sy)
def create_mesh():
radial_angles = np.linspace(0, 2 * np.pi, TRIANGLE_VERTEX_COUNT, endpoint=False)
vertices_2d = np.column_stack(
(R * np.cos(radial_angles), R * np.sin(radial_angles))
)
for i in range(ORDER):
vertices_2d = _increase_order(vertices_2d)
return linear_extrude(
sg.Polygon(vertices_2d),
height=HEIGHT,
center=False,
convexity=ORDER,
twist=TWIST,
slices=SLICES,
scale=SCALE,
)
def _increase_order(vertices_2d):
vertices_2d = [
add_midpoint_bump(
edges[0],
edges[1],
KOCH_DEFAULT_BUMP_LENGTH_FRACTION * BUMP_LENGTH_FRACTION_MULTIPLIER,
)
for edges in zip(vertices_2d, np.roll(vertices_2d, -1, axis=0))
]
return np.concatenate(vertices_2d, axis=0)
def add_midpoint_bump(
vertex_a, vertex_b, new_edge_length_fraction=KOCH_DEFAULT_BUMP_LENGTH_FRACTION
):
midpoint = (vertex_a + vertex_b) / 2.0
delta_vector = vertex_b - vertex_a
edge_length = np.linalg.norm(delta_vector)
new_edge_length = edge_length * new_edge_length_fraction
# koch adds 2 points, each at 1/3 of the edge length, both 1/6 from the midpoint
perp_vector_needed_length = np.sqrt(new_edge_length**2 - (edge_length / 6.0) ** 2)
perp_vector = np.array([delta_vector[1], -delta_vector[0]])
perp_vector /= np.linalg.norm(perp_vector)
perp_vector *= perp_vector_needed_length # Normalize to unit length
new_vertex_0 = vertex_a + delta_vector / 3.0
new_vertex_1 = midpoint + perp_vector
new_vertex_2 = vertex_b - delta_vector / 3.0
return np.array([vertex_a, new_vertex_0, new_vertex_1, new_vertex_2])
if __name__ == "__main__":
create_mesh()

lego.py
Procedurally builds a Lego-style brick with pegs and under-cylinders, yielding intermediate steps as it unions geometry. Demonstrates a complex parametric build with incremental yields.
Source
from trimesh.creation import box, cylinder
from trimesh.transformations import translation_matrix
# Dimensions of the brick (H)/ plate (h)
DIMS = (4, 8)
IS_PLATE = False
class LegoBrick:
# Lego constants
P = 8.0 # multiplier for the peg size
h = 3.2 # height of a plate
H = 3 * h # height of a brick
# INTER_PEG_DISTANCE = 8.0
VERTICAL_WALL_THICKNESS = 1.5
INNER_VERTICAL_WALL_THICKNESS = 0.8
INNER_VERTICAL_WALL_HEIGHT = 6.3
HORIZONTAL_WALL_THICKNESS = 1.0
PEG_D = 4.8 + 0.1 # since my printer seems to make it too small
PEG_H = 1.8
UNDER_PEG_D = 2.6
UNDER_CIRCLE_ID = 4.8
UNDER_CIRCLE_OD = 6.5
INTER_BRICK_GAP = 0.2
def __init__(self, peg_dims, is_plate=False):
self.peg_dims = peg_dims
self.is_plate = is_plate
self.brick = self.create_mesh()
def create_mesh(self):
# Create a box
brk_dims = self.brick_dims()
brick = box(brk_dims)
yield brick
# Move bottom of the box to the origin
bx_translation = translation_matrix([dim / 2 for dim in brk_dims])
inner_brk_dims = self.inner_brick_dims()
inner_brick = box(inner_brk_dims)
yield inner_brick
move_by = [dim / 2 for dim in inner_brk_dims]
move_by[0] = move_by[0] + self.VERTICAL_WALL_THICKNESS
move_by[1] = move_by[1] + self.VERTICAL_WALL_THICKNESS
move_by[2] = inner_brk_dims[2] / 2
inner_brick_translation = translation_matrix(move_by)
brick.apply_transform(bx_translation)
inner_brick.apply_transform(inner_brick_translation)
brick = brick.difference(inner_brick)
yield brick
for i in range(self.peg_dims[0]):
for j in range(self.peg_dims[1]):
peg = self.create_peg_at((i, j))
brick = brick.union(peg)
yield brick
for i in range(self.peg_dims[0] - 1):
for j in range(self.peg_dims[1] - 1):
under_cyl = self.create_under_cylinder_at((i, j))
brick = brick.union(under_cyl)
yield brick
# return brick
def brick_dims(self):
return [
self.P * self.peg_dims[0] - self.INTER_BRICK_GAP,
self.P * self.peg_dims[1] - self.INTER_BRICK_GAP,
self.height(),
]
def height(self):
return self.h if self.is_plate else self.H
def inner_brick_dims(self):
brk_dims = self.brick_dims()
return [
brk_dims[0] - 2 * self.VERTICAL_WALL_THICKNESS,
brk_dims[1] - 2 * self.VERTICAL_WALL_THICKNESS,
brk_dims[2] - self.HORIZONTAL_WALL_THICKNESS,
]
def create_peg_at(self, peg_grid):
peg = cylinder(self.PEG_D / 2, self.PEG_H)
peg_translation = translation_matrix(
[
self.P * (peg_grid[0] + 0.5) - self.INTER_BRICK_GAP / 2,
self.P * (peg_grid[1] + 0.5) - self.INTER_BRICK_GAP / 2,
self.height() + self.PEG_H / 2,
]
)
peg.apply_transform(peg_translation)
return peg
def create_under_cylinder_at(self, cyl_grid):
cyl_height = self.height() - self.HORIZONTAL_WALL_THICKNESS
cyl = cylinder(self.UNDER_CIRCLE_OD / 2, cyl_height)
inner_cl = cylinder(self.UNDER_CIRCLE_ID / 2, cyl_height)
cyl = cyl.difference(inner_cl)
cyl_translation = translation_matrix(
[
self.P * (cyl_grid[0] + 1.0) - self.INTER_BRICK_GAP / 2,
self.P * (cyl_grid[1] + 1.0) - self.INTER_BRICK_GAP / 2,
cyl_height / 2,
]
)
cyl.apply_transform(cyl_translation)
return cyl
def create_mesh():
brick = LegoBrick(DIMS, IS_PLATE)
return brick.create_mesh()

messner_mani.py
Uses manifold3d to carve a cube into a Menger-sponge-like structure by repeatedly subtracting smaller cubes. Yields intermediate meshes as a list to visualize each step in debug mode.
Source
from manifold3d import Manifold
def create_mesh():
for mesh in create_messner(27, 3):
latest = mesh
yield mesh
return latest
def create_messner(extent=1, n=1):
mesh = Manifold.cube([extent, extent, extent], True)
for i in range(0, n):
grid_points_per_line = 3**i
diff_extent = extent / grid_points_per_line / 3.0
step = diff_extent * 3.0
start = (-extent + diff_extent * 3) / 2.0
for j in range(0, grid_points_per_line):
x = start + j * step
for k in range(0, grid_points_per_line):
if j % 3 == 1 and k % 3 == 1:
continue
y = start + k * step
bx = Manifold.cube([diff_extent, diff_extent, extent], True)
bx = bx.translate([x, y, 0])
mesh -= bx
yield mesh
bx = Manifold.cube([diff_extent, extent, diff_extent], True)
bx = bx.translate([x, 0, y])
mesh -= bx
yield mesh
bx = Manifold.cube([extent, diff_extent, diff_extent], True)
bx = bx.translate([0, x, y])
mesh -= bx
yield mesh

mobius.py
Sweeps a thin rectangle along a circular path with a half twist to form a Mobius strip. Shows sweep_polygon with twist and slice control.
Source
import numpy as np
from shapely import box
from trimesh.creation import sweep_polygon
WIDTH = 10.0
THICK = 0.5
SLICES = 100
OVERSHOOT = (
SLICES + 1
) / SLICES # Overshoot by a slice along the path to close properly
MID_RADIUS = 30.0 # Radius of the strip along the center line
def create_mesh():
bx = box(-WIDTH / 2.0, -THICK / 2.0, WIDTH / 2.0, THICK / 2.0)
theta = np.linspace(0, 2 * np.pi * (SLICES + 1) / SLICES, SLICES)
twist = np.linspace(0, np.pi * (SLICES + 1) / SLICES, SLICES)
circle = MID_RADIUS * np.column_stack(
(
np.cos(theta),
np.sin(theta),
np.zeros(SLICES),
)
)
return sweep_polygon(bx, circle, twist, connect=True, cap=False)

mushroom.py
Builds a mushroom-like shape using icospheres, annuli, and booleans. It sets logging to DEBUG and includes metadata-based coloring notes.
Source
import logging
from trimesh.creation import annulus, box, icosphere
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
MILLIS_PER_INCH = 25.4
STEM_R_MAX = 6.5 / 2.0
LIP = 0.125
TOP_R = 7.25 / 2.0
def create_mesh():
logging.debug("Creating mushroom mesh")
top = icosphere(radius=TOP_R * MILLIS_PER_INCH).apply_scale([1.0, 1.0, 0.5])
cut = box(
[9 * MILLIS_PER_INCH, 9 * MILLIS_PER_INCH, 4.5 * MILLIS_PER_INCH]
).apply_translation([0.0, 0.0, -4.5 / 2 * MILLIS_PER_INCH])
cut.metadata = {"scadview": {"color": [0.0, 1.0, 0.0, 0.3]}}
inner_top = icosphere(radius=(TOP_R - 0.1) * MILLIS_PER_INCH).apply_scale(
[1.0, 1.0, 0.5]
)
top = top.difference(inner_top).difference(cut)
stem = annulus(
r_max=STEM_R_MAX * MILLIS_PER_INCH,
r_min=(STEM_R_MAX - 0.1) * MILLIS_PER_INCH,
height=2.0 * MILLIS_PER_INCH / 2.0,
).apply_translation([0.0, 0.0, -2.0 * MILLIS_PER_INCH / 4.0])
join = annulus(
r_max=TOP_R * MILLIS_PER_INCH,
r_min=(STEM_R_MAX - 0.1) * MILLIS_PER_INCH,
height=0.1 * MILLIS_PER_INCH,
)
edge = annulus(
r_max=(STEM_R_MAX + 0.125) * MILLIS_PER_INCH,
r_min=(STEM_R_MAX) * MILLIS_PER_INCH,
height=0.1 * MILLIS_PER_INCH,
).apply_translation(
[
0.0,
0.0,
-2.0 * MILLIS_PER_INCH / 2.0,
]
)
return top.union(stem).union(join).union(edge)

simple_animation.py
A generator that yields a moving box with sleep(...) between frames. This is the minimal animation example.
Source
from time import sleep
from trimesh.creation import box
def create_mesh():
b = box([10, 10, 20])
for _ in range(100):
yield b
sleep(0.1)
b.apply_translation([0.2, 0, 0])

sphere.py
Returns a single icosphere. This is the simplest possible create_mesh.
Source
from trimesh.creation import icosphere
def create_mesh():
return icosphere(radius=40, subdivisions=2)

star_linear_extrude.py
Constructs a 2D star polygon with an inner hole and extrudes it with twist and taper. Demonstrates linear_extrude parameters like twist, slices, and scale.
Source
import logging
import numpy as np
import shapely.geometry as sg
from scadview import linear_extrude
POINTS = 5
R1, R2 = 1.0, 2.0
INNER_SCALE = 0.5
HEIGHT = 10.0
SLICES = 120
EXTRUDE_SCALE = 2.5
TWIST_ANGLE = 270
def create_mesh():
# simple 2D star to demo twist/taper; will be projected if 3D is passed
star = [
(
(
R2 * np.cos(2 * np.pi * i / (2 * POINTS)),
R2 * np.sin(2 * np.pi * i / (2 * POINTS)),
)
if i % 2 == 0
else (
R1 * np.cos(2 * np.pi * i / (2 * POINTS)),
R1 * np.sin(2 * np.pi * i / (2 * POINTS)),
)
)
for i in range(2 * POINTS)
]
inner_star = [(INNER_SCALE * x, INNER_SCALE * y) for x, y in star]
poly = sg.Polygon(star, [inner_star])
return linear_extrude(
poly,
height=HEIGHT, # OpenSCAD: required
center=True, # OpenSCAD default
# convexity=10, # accepted/ignored
twist=TWIST_ANGLE, # total degrees
slices=SLICES, # use fn if given; else 20
scale=EXTRUDE_SCALE, # scalar or (sx, sy)
# fn=10, # optional OpenSCAD-like override for slices
)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
mesh = create_mesh()

surface_bend.py
Loads a heightmap from the splash image using surface(...), then bends its vertices around an arc and rebuilds a Trimesh. This example shows post-processing vertex positions and applying transforms.
Source
import os
import numpy as np
from trimesh import Trimesh, transformations
from scadview import surface
def create_mesh():
pixel_size = (388, 400)
desired_x_size = 200.0
height = 20.0
scale = desired_x_size / pixel_size[0]
parent_dir = os.path.dirname(os.path.abspath(__file__))
mesh = surface(
os.path.join(parent_dir, "../src/scadview/resources/splash.png"),
scale=(scale, scale, height),
invert=True,
base=0.0,
)
# return mesh
curve = 2 * np.pi # / 6.0
curved_verts = bend_x(mesh.vertices, arc_radians=curve)
curved_mesh = Trimesh(vertices=curved_verts, faces=mesh.faces)
# rotate about the y axis so curved mesh edges lie on the xy plane (90 degrees - curve in degreees / 2.0)
rot = transformations.rotation_matrix(
angle=np.pi / 2.0 - curve / 2.0,
direction=[0, 1, 0],
point=(0, 0, 0),
)
curved_mesh.apply_transform(rot)
return curved_mesh
def bend_x(vertices: np.ndarray, arc_radians: float = np.pi / 2.0) -> np.ndarray:
"""
Bend the mesh along the x-axis.
The x coords are mapped to an arc in the xz plane with the given inner radius.
The arc starts at the minimum x value, and its length is equal to the range of x values in the mesh.
rad_x = arc_radians * (x - min(x)) / (max(x) - min(x))
inner_radius is computed so that inner_radius * arc_radians is the length of the arc.
So inner_radius = (max(x) - min(x)) / arc_radians.
x = (inner_radius + z) * np.cos(rad_x) + (max(x) - min(x)) / 2
z = (inner_radius + z) * np.sin(rad_x)
y is unchanged.
"""
x, y, z = vertices.T
print(np.shape(vertices), np.shape(x), np.shape(y), np.shape(z))
x_min = np.min(x)
x_max = np.max(x)
x_span = x_max - x_min
print(f"x_min: {x_min}, x_max: {x_max}, x_span: {x_span}")
inner_radius = x_span / arc_radians
rad_x = arc_radians * (x - x_min) / x_span
x_new = -(inner_radius + z) * np.cos(rad_x) + inner_radius
z_new = (inner_radius + z) * np.sin(rad_x)
print(f"x_new: {np.shape(x_new)}, z_new: {np.shape(z_new)}")
return np.stack((x_new, y, z_new), axis=-1)
def recompute_trimesh(mesh):
mesh._cache.clear()
mesh.fix_normals()
if __name__ == "__main__":
mesh = create_mesh()

surface_.py
Loads a heightmap from the splash image using surface(...).
Source
import os
from scadview import surface
def create_mesh():
pixel_size = (388, 400)
desired_x_size = 200.0
height = 10.0
scale = desired_x_size / pixel_size[0]
parent_dir = os.path.dirname(os.path.abspath(__file__))
mesh = surface(
os.path.join(parent_dir, "../src/scadview/resources/splash.png"),
scale=(scale, scale, height),
invert=True,
base=0.0,
)
return mesh

text.py
Uses text(...) with a specific font and alignment options, then scales in Z to add thickness. Demonstrates text mesh creation and font selection.
Source
from scadview import text
def create_mesh():
t = "SCADview"
font = "Papyrus:style=Condensed"
return text(
t,
font=font,
size=100,
valign="bottom",
halign="right",
direction="ltr",
).apply_scale((1.0, 1.0, 10.0))

toothbrush_holder.py
A full parametric design that yields intermediate meshes as it builds a multi-tube holder with labeled name plates and a base. It demonstrates incremental yield updates, boolean unions/differences, and procedural layout.
Source
from typing import Generator
import numpy as np
from shapely.geometry import Polygon
from trimesh import Trimesh, transformations
from trimesh.creation import box, cylinder, extrude_polygon
from scadview import text
"""
A toothbrush holder with a tube for each person in the household.
Each tube is made from a hexagonal grid bent into a tube shape.
Each tube is labeled with the person's name.
The tubes are arrange in a circle on a circular base plate.
The center axis of each tube extends from a circle on the base plate,
to a circle at the top of the tube.
Both circles have the same radius, and are centered on the same axis
as the base plate.
The tubes are tilted to hold the toothbrushes at an angle.
"""
# Names to mark each holder tube
NAMES = [
"NEIL",
"ADAM",
"SOPHIE",
]
TUBE_INNER_R = 9.5 # Tube size for a toothbrush (radius)
TUBE_H = 100 # height of tube when standing upright
TILT = np.deg2rad(-30) # tilt of tube
TUBE_COUNT = len(NAMES) # number of toothbrushes to hold
TUBE_WALL = 3 # Thickness of the tube wall
HEX_GRID_COLS = 8 # Count of cols in the hex grid that makes the tube
GRID_DIV_WIDTH = 1.5 # Thickeness of the divisions between the hex cells
POSITIONING_R = (
28 # Radius of the circle on which the center of the tubes tops / bottoms are positioned
# If the tubes intersect too deeply, increase this.
)
# Radius of the circle the center of the tubes are positioned on
# (position_r)**2 = center_positioning_r**2 + (sin(tilt) * tube_h / 2)**2
CENTER_POSITIONING_R = np.sqrt(
np.square(POSITIONING_R) - np.square(np.sin(TILT) * TUBE_H / 2)
)
FONT = "Helvetica" # Font for names
FONT_H = TUBE_WALL # Extruded height of the font
FONT_SIZE = 8 # font "size"
NAME_PLATE_H = 3 # Height of base under name
NAME_PLATE_BORDER = 2
BASE_R = 41 # Radius of the base - make large enough so that the tube bottom ends are completely embedded in the base
BASE_H = 5 # Height (thickness) of the base
def create_mesh() -> (
Trimesh | list[Trimesh] | Generator[Trimesh | list[Trimesh], None, None]
):
hex_grid = make_grid(TUBE_INNER_R, TUBE_H, TUBE_WALL, HEX_GRID_COLS, GRID_DIV_WIDTH)
yield hex_grid
# Each tube is moved radially in evenly spaced directions around a full circle
tube_move_directions = np.linspace(0.0, 2 * np.pi, len(NAMES) + 1)
mesh = Trimesh() # Keep the type checking happy
mesh_started = False
for name, direction in zip(NAMES, tube_move_directions):
name_mesh = text(
name,
FONT_SIZE,
font=FONT,
halign="center",
).apply_scale([1, 1, FONT_H])
yield name_mesh
grid_mesh = add_name(
hex_grid.copy(),
name_mesh,
NAME_PLATE_H,
NAME_PLATE_BORDER,
)
yield grid_mesh
tube = build_tube(grid_mesh, TUBE_H, TILT, CENTER_POSITIONING_R, direction)
if not mesh_started:
mesh = tube
mesh_started = True
else:
mesh = mesh.union(tube)
yield mesh
base_top_at = (TUBE_INNER_R + TUBE_WALL + GRID_DIV_WIDTH * 2) * np.sin(-TILT)
base = cylinder(BASE_R, BASE_H).apply_translation([0, 0, base_top_at - BASE_H / 2])
mesh = mesh.union(base)
yield mesh
# cut off anything extruding past the bottom of th base
cut_height = TUBE_H # some high number
cut_top_at = base_top_at - BASE_H
cut_r = BASE_R * 1.1 # something bigger than the base radius
cut = cylinder(cut_r, cut_height).apply_translation(
[0, 0, cut_top_at - cut_height / 2]
)
yield mesh.difference(cut)
def make_grid(
tube_inner_r: float,
tube_h: float,
tube_wall: float,
grid_cols: int,
grid_div_width: float,
):
hex_cell_dims, rows = hex_cell_dims_for_tube(
tube_inner_r, tube_h, tube_wall, grid_div_width, grid_cols
)
grid = hex_grid(hex_cell_dims, grid_cols, rows, grid_div_width)
frame_dims = hex_grid_dims(hex_cell_dims, grid_cols, rows)
bottom_frame = box(
(frame_dims[0], grid_div_width, frame_dims[2])
).apply_translation((frame_dims[0] / 2, 0, frame_dims[2] / 2))
top_frame = bottom_frame.copy().apply_translation((0, frame_dims[1], 0))
return (
grid.union(top_frame)
.union(bottom_frame)
.apply_translation([0, grid_div_width / 2, 0])
)
def hex_cell_dims_for_tube(
tube_inner_r: float,
tube_h: float,
tube_wall: float,
grid_div_width: float,
cols: int,
) -> tuple[tuple[float, float, float], int]:
# cols must be even for this to work.
# The last col is only .75 wide so it can mate with its
# other side when bent into a tube
# 2 * pi * tube_inner_r = cols * cell_dim_x * 0.75
# 2 * pi * tube_inner_r = cell_dim_x * ( 0.75 * (cols - 1) + 1)
# 2 * pi * tube_inner_r = cell_dim_x * ( 0.75 * cols + 0.25)
# so cell_dim_x = 2 * pi * tube_inner_r / (cols * .75)
cell_dim_x = 2 * np.pi * tube_inner_r / (cols * 0.75)
rows = round(tube_h / cell_dim_x)
cell_dim_y = (tube_h - grid_div_width) / rows
return (cell_dim_x, cell_dim_y, tube_wall), rows
def add_name(
grid_mesh: Trimesh,
name_mesh: Trimesh,
name_plate_h: float,
name_plate_border: float,
) -> Trimesh:
"""
Place name so that it will run vertically along the tube.
Add a plate behind it.
"""
name_mesh_dims = name_mesh.bounds[1] - name_mesh.bounds[0]
base_dims = [
name_mesh_dims[0] + name_plate_border, # Only add the border at end of name
# Add border on top and bottom of name:
name_mesh_dims[1] + name_plate_border * 2,
name_plate_h,
]
base = box(base_dims).apply_translation(
(2, base_dims[1] / 2 - name_plate_border, -base_dims[2] / 2)
)
name_mesh = name_mesh.union(base)
name_mesh.apply_translation((name_mesh_dims[0] / 2, 0, name_plate_h))
name_mesh.apply_transform(
transformations.rotation_matrix(-np.pi / 2, [0, 0, 1], [0, 0, 0])
)
grid_mesh_center = (grid_mesh.bounds[1] + grid_mesh.bounds[0]) / 2
name_mesh.apply_translation((grid_mesh_center[0], grid_mesh.bounds[1, 1], 0))
return grid_mesh.union(name_mesh)
def hex_grid(
cell_dims: tuple[float, float, float],
cols: int,
rows: int,
wall_width: float,
) -> Trimesh:
"""
Create a hexagonal grid of cells with the given dimensions, number of columns and rows, and wall width.
Each cell is a hexagon with a bounding box of the given cell dimensions.
An inner hexagon is punched out of each cell to create the walls.
The grid lies flat in the XY plane, with the Z axis pointing up.
The hexagons are oriented such that the flat sides are parallel to the X axis.
The first column's left hex vertices touch the Y axis, and the first row's bottom first hex vertices touch the X axis.
The hexagons are arranged in a staggered pattern, with every second column offset by half the height of a hexagon.
The cell dims are the dimension of the bounding box of the hexagon, not the hexagon itself.
The wall width is the width of the walls between the cells.
Horizontal walls are the flat sides of the hexagons, and vertical walls are the angled sides.
Horizonal wall have the wall thickness in the y dimension.
Vertical walls thickness is along x direction
"""
base_dims = hex_grid_dims(cell_dims, cols, rows)
grid = box(base_dims).apply_translation(
(base_dims[0] / 2, base_dims[1] / 2, base_dims[2] / 2)
)
hole_dims = (
cell_dims[0] - wall_width,
cell_dims[1] - wall_width,
cell_dims[2],
)
base_hole = hexagon(hole_dims)
starting_offset = (
cell_dims[0] / 2,
cell_dims[1] / 2,
)
for row in range(-1, rows):
for col in range(-1, cols + 1):
offset = (
col * cell_dims[0] * 0.75 + starting_offset[0],
row * cell_dims[1] + starting_offset[1],
0,
)
if col % 2 == 1:
offset = (
offset[0],
offset[1] + cell_dims[1] / 2,
0,
)
inner_hex = base_hole.copy().apply_translation(offset)
grid = grid.difference(inner_hex)
return grid
def hexagon(
cell_dims: tuple[float, float, float],
) -> Trimesh:
# 6 vertices within the xy bounds extruded to the z dimension
angles = np.linspace(0, 2 * np.pi, 7)[:-1] # drop last to avoid duplicate
points = np.column_stack((np.cos(angles), np.sin(angles)))
x_span = max(points[:, 0]) - min(points[:, 0])
y_span = max(points[:, 1]) - min(points[:, 1])
x_scale = cell_dims[0] / x_span
y_scale = cell_dims[1] / y_span
points = points * np.array([x_scale, y_scale])
return extrude_polygon(Polygon(points), cell_dims[2])
def hex_grid_dims(
cell_dims: tuple[float, float, float],
cols: int,
rows: int,
) -> tuple[float, float, float]:
return (
cols * cell_dims[0] * 0.75,
rows * cell_dims[1],
cell_dims[2],
)
def build_tube(
grid_mesh: Trimesh,
tube_h: float,
tilt: float,
center_positioning_r: float,
direction: float,
) -> Trimesh:
tube = bend_grid(
grid_mesh,
)
# Stand upright
tube.apply_transform(
transformations.rotation_matrix(np.pi / 2, [1, 0, 0], [0, 0, 0])
)
# Rotate to move name to outside
tube.apply_transform(transformations.rotation_matrix(np.pi, [0, 0, 1], [0, 0, 0]))
# Move center to origin
tube.apply_translation((0, 0, -tube_h / 2))
# Tilt
tube.apply_transform(transformations.rotation_matrix(tilt, [1, 0, 0], [0, 0, 0]))
# rotate around z
tube.apply_transform(
transformations.rotation_matrix(direction, [0, 0, 1], [0, 0, 0])
)
# Move bottom center to XY plane and push out to c center to CENTER_POSITIONING_R
tube.apply_translation(
(
-center_positioning_r * np.cos(direction),
-center_positioning_r * np.sin(direction),
np.cos(tilt) * tube_h / 2,
)
)
return tube
def bend_grid(
grid: Trimesh,
) -> Trimesh:
grid = grid.subdivide().subdivide().subdivide().subdivide()
bend = 2 * np.pi
bent_verts = bend_x(
grid.vertices,
arc_radians=bend,
)
return Trimesh(vertices=bent_verts, faces=grid.faces)
def bend_x(
vertices: np.ndarray, arc_radians: float = np.pi / 2.0, x_gap=0.001
) -> np.ndarray:
"""
Bend the mesh along the x-axis.
The x coords are mapped to an arc in the xz plane.
The arc starts at the minimum x value, and its length is equal to the range of x values in the mesh.
rad_x = arc_radians * (x - min(x)) / (max(x) - min(x))
inner_radius is computed so that inner_radius * arc_radians is the length of the arc.
So inner_radius = (max(x) - min(x)) / arc_radians.
x = (inner_radius + z) * np.cos(rad_x) + (max(x) - min(x)) / 2
z = (inner_radius + z) * np.sin(rad_x)
y is unchanged.
"""
x, y, z = vertices.T
x_min = np.min(x)
x_max = np.max(x)
x_span = x_max - x_min + x_gap
inner_radius = x_span / arc_radians
rad_x = arc_radians * (x - x_min) / x_span
x_new = -(inner_radius + z) * np.cos(rad_x) # + inner_radius
z_new = (inner_radius + z) * np.sin(rad_x)
return np.stack((x_new, y, z_new), axis=-1)

xyz.py
A generator that continuously rotates a frame with cut-out X/Y/Z letters and yields tinted meshes. Demonstrates infinite generators, transforms, booleans, and set_mesh_color.
Source
from datetime import datetime
import numpy as np
from trimesh.creation import box
from trimesh.transformations import rotation_matrix
from scadview import Color, set_mesh_color, text
LETTER_DEPTH = 10
FRAME = 2
RPS = 0.3
ALPHA = 0.5
def create_mesh():
x = center_mesh(
create_letter("X", LETTER_DEPTH).apply_transform(
rotation_matrix(np.pi / 2, (1, 0, 0))
@ rotation_matrix(np.pi / 2, (0, 1, 0))
)
)
y = center_mesh(
create_letter("Y", LETTER_DEPTH).apply_transform(
rotation_matrix(np.pi / 2, (1, 0, 0))
)
)
z = center_mesh(create_letter("Z", LETTER_DEPTH))
frame_dims = np.max(
[
x.extents + np.array((0, 1, 1)) * FRAME,
y.extents + np.array((1, 0, 1)) * FRAME,
z.extents + np.array((1, 1, 0)) * FRAME,
],
axis=1,
)
x.apply_scale((1.1 * frame_dims[0] / LETTER_DEPTH, 1, 1))
y.apply_scale((1, 1.1 * frame_dims[1] / LETTER_DEPTH, 1))
z.apply_scale((1, 1, 1.1 * frame_dims[2] / LETTER_DEPTH))
start_time = datetime.now()
frame = box(frame_dims)
while True:
yield set_mesh_color(
frame.difference(x.union(y).union(z)).apply_transform(
rotation_matrix(
2 * np.pi * (datetime.now() - start_time).total_seconds() * RPS,
(1.0, 0.5, 0.3),
)
),
Color.SILVER,
ALPHA,
)
def create_letter(letter: str, depth: float):
return text(letter).apply_scale((1.0, 1.0, depth))
def center_mesh(mesh):
center = (mesh.bounds[0] + mesh.bounds[1]) / 2
return mesh.apply_translation(-center)

xyz_cube.py
Builds a cube with extruded X/Y/Z letters on each face and cuts the opposite letters to form a labeled axis cube. Shows text meshes, transformations, and boolean operations.
Source
import numpy as np
from pyrr.matrix44 import create_from_axis_rotation
from trimesh import Trimesh
from trimesh.creation import box
from scadview import text
SIZE = 50
TEXT_FRACTION = 0.8
TEXT_HEIGHT = SIZE / 10
TEXT_SHRINK_FACTOR = 0.15
def create_mesh():
box_mesh = box(extents=(SIZE, SIZE, SIZE))
x_mesh = text("X", halign="center", size=SIZE)
y_mesh = text("Y", halign="center", size=SIZE)
z_mesh = text("Z", halign="center", size=SIZE)
max_xy_extent = max(
x_mesh.bounds[0:2].max(), y_mesh.extents[0:2].max(), z_mesh.extents[0:2].max()
)
scale_xy = (SIZE / max_xy_extent) * TEXT_FRACTION
scale = (scale_xy, scale_xy, TEXT_HEIGHT)
x_mesh.apply_scale(scale)
y_mesh.apply_scale(scale)
z_mesh.apply_scale(scale)
x_mesh = shrink_towards_top(x_mesh, TEXT_SHRINK_FACTOR)
y_mesh = shrink_towards_top(y_mesh, TEXT_SHRINK_FACTOR)
z_mesh = shrink_towards_top(z_mesh, TEXT_SHRINK_FACTOR)
xy_center_mesh(x_mesh)
x_mesh.apply_transform(
create_from_axis_rotation((1, 0, 0), np.pi / 2)
).apply_transform(
create_from_axis_rotation((0, 0, 1), np.pi / 2)
).apply_translation((SIZE / 2, 0, 0))
xy_center_mesh(y_mesh)
y_mesh.apply_transform(
create_from_axis_rotation((1, 0, 0), np.pi / 2)
).apply_translation((0, SIZE / 2, 0)).apply_transform(
create_from_axis_rotation((0, 1, 0), np.pi)
)
xy_center_mesh(z_mesh)
z_mesh.apply_translation((0, 0, SIZE / 2))
x_bot_mesh = x_mesh.copy().apply_translation((-SIZE, 0, 0))
y_bot_mesh = y_mesh.copy().apply_translation((0, -SIZE, 0))
z_bot_mesh = z_mesh.copy().apply_translation((0, 0, -SIZE))
return (
box_mesh.union(x_mesh)
.union(y_mesh)
.union(z_mesh)
.difference(x_bot_mesh)
.difference(y_bot_mesh)
.difference(z_bot_mesh)
)
def xy_center_mesh(mesh: Trimesh) -> Trimesh:
bounds = mesh.bounds
center = (bounds[0] + bounds[1]) / 2
mesh.apply_translation([-center[0], -center[1], 0])
return mesh
def shrink_towards_top(mesh: Trimesh, factor: float) -> Trimesh:
"""Shrink the mesh towards its top face by the given factor (0 to 1)."""
centroid = mesh.centroid
vertices = mesh.vertices.copy()
for i, v in enumerate(vertices):
# Calculate how far along the Z axis this vertex is (0 at base, 1 at top)
t = factor * (v[2] - mesh.bounds[0][2]) / TEXT_HEIGHT
new_xy = (1 - t) * v[:2] + t * centroid[:2]
vertices[i][:2] = new_xy
mesh.vertices = vertices
return mesh
