VTK Integration
These examples demonstrate how to integrate trueform into VTK applications for real-time interactive geometry processing.
Integration Pattern
The examples use helper functions (see util/geometry.py) for VTK integration:
From VTK to trueform
import vtk
from vtk.util import numpy_support
import numpy as np
import trueform as tf
def polydata_to_numpy(polydata: vtk.vtkPolyData):
"""Extract numpy arrays from VTK PolyData"""
# Get points (zero-copy view)
points = numpy_support.vtk_to_numpy(polydata.GetPoints().GetData())
# Get triangle connectivity
cells = polydata.GetPolys()
connectivity = numpy_support.vtk_to_numpy(cells.GetConnectivityArray())
faces = connectivity.reshape(-1, 3).astype(np.int32)
return faces, points
# Usage
polydata = reader.GetOutput()
faces, points = polydata_to_numpy(polydata)
mesh = tf.Mesh(faces, points.astype(np.float32))
From trueform to VTK
import vtk
from vtk.util import numpy_support
import numpy as np
import trueform as tf
def numpy_to_polydata(points: np.ndarray, triangles: np.ndarray) -> vtk.vtkPolyData:
"""Convert numpy arrays to VTK PolyData"""
vtk_points = vtk.vtkPoints()
vtk_points.SetData(numpy_support.numpy_to_vtk(
num_array=points, deep=True,
array_type=vtk.VTK_FLOAT if points.dtype == np.float32 else vtk.VTK_DOUBLE
))
n_tris = triangles.shape[0]
connectivity = triangles.astype(np.int64).ravel()
offsets = np.arange(0, 3 * n_tris + 1, 3, dtype=np.int64)
cell_array = vtk.vtkCellArray()
cell_array.SetData(
numpy_support.numpy_to_vtkIdTypeArray(offsets, deep=True),
numpy_support.numpy_to_vtkIdTypeArray(connectivity, deep=True)
)
poly = vtk.vtkPolyData()
poly.SetPoints(vtk_points)
poly.SetPolys(cell_array)
return poly
def curves_to_polydata(paths, points: np.ndarray) -> vtk.vtkPolyData:
"""Convert trueform curves to VTK PolyData"""
vtk_points = vtk.vtkPoints()
vtk_points.SetData(numpy_support.numpy_to_vtk(
num_array=points, deep=False,
array_type=vtk.VTK_FLOAT if points.dtype == np.float32 else vtk.VTK_DOUBLE
))
# paths.offsets and paths.data are already in VTK format
lines = vtk.vtkCellArray()
lines.SetData(
numpy_support.numpy_to_vtkIdTypeArray(paths.offsets.astype(np.int64), deep=True),
numpy_support.numpy_to_vtkIdTypeArray(paths.data.astype(np.int64), deep=True)
)
poly = vtk.vtkPolyData()
poly.SetPoints(vtk_points)
poly.SetLines(lines)
return poly
Collision Detection
Source: vtk/collision.py
Interactive application with a 5x5 grid of meshes. Hover to highlight, click and drag to move. Colliding meshes turn cyan.
python collision.py mesh.stl [mesh2.stl ...]
Features Showcased
- Building spatial trees with
mesh.build_tree() - Collision detection with
tf.intersects(mesh1, mesh2) - Dynamic transformations via
mesh.transformation
Loading and Positioning
import trueform as tf
import numpy as np
# Load mesh
faces, points = tf.read_stl("mesh.stl")
mesh = tf.Mesh(faces, points)
mesh.build_tree()
# Create transformation (center, scale, rotate, position)
transform = np.eye(4, dtype=np.float32)
transform[:3, :3] = rotation_matrix
transform[:3, 3] = position
mesh.transformation = transform
# Sync VTK actor with trueform transformation
matrix = vtk.vtkMatrix4x4()
for i in range(4):
for j in range(4):
matrix.SetElement(i, j, transform[i, j])
actor.SetUserMatrix(matrix)
Collision Detection Loop
def check_collisions(meshes, actors):
for i, mesh_i in enumerate(meshes):
colliding = False
for j, mesh_j in enumerate(meshes):
if i != j and tf.intersects(mesh_i, mesh_j):
colliding = True
break
if colliding:
actors[i].GetProperty().SetColor(0.0, 1.0, 1.0) # Cyan
else:
actors[i].GetProperty().SetColor(1.0, 1.0, 1.0) # White
Updating Transform During Drag
def on_mouse_move(mesh, actor, delta):
# Get current transform
transform = mesh.transformation.copy()
# Update position
transform[0, 3] += delta[0]
transform[1, 3] += delta[1]
transform[2, 3] += delta[2]
# Apply to trueform (tree is reused)
mesh.transformation = transform
# Sync VTK actor
for i in range(4):
for j in range(4):
actor.GetUserMatrix().SetElement(i, j, transform[i, j])
Boolean Difference
Source: vtk/boolean_difference.py
Dual viewport showing input meshes with intersection curves (left) and boolean difference result (right). Drag meshes to interact, press n to randomize orientations.
python boolean_difference.py mesh1.stl [mesh2.stl]
Features Showcased
- Boolean operations with
tf.boolean_difference - Extracting intersection curves with
return_curves=True - Real-time updates during interaction
Boolean Computation
import trueform as tf
# Compute difference with curves
(result_faces, result_points), labels, (paths, curve_points) = (
tf.boolean_difference(mesh1, mesh2, return_curves=True)
)
# Convert result to VTK
result_polydata = numpy_to_polydata(result_points, result_faces)
curve_polydata = curves_to_polydata(paths, curve_points)
Real-time Update Callback
def on_interaction():
# Recompute boolean with current transforms
(faces, points), labels, (paths, curve_pts) = (
tf.boolean_difference(mesh1, mesh2, return_curves=True)
)
# Update VTK visualization
result_polydata = numpy_to_polydata(points, faces)
result_mapper.SetInputData(result_polydata)
curve_polydata = curves_to_polydata(paths, curve_pts)
curve_mapper.SetInputData(curve_polydata)
render_window.Render()
Intersection Curves
Source: vtk/intersection_curves.py
Real-time visualization of intersection curves between two moving meshes.
python intersection_curves.py mesh1.stl [mesh2.stl]
Computing Curves
import trueform as tf
# Compute intersection curves
paths, curve_points = tf.intersection_curves(mesh1, mesh2)
# paths: OffsetBlockedArray of vertex indices
# curve_points: numpy array of 3D points
# Iterate over curves
for path_ids in paths:
pts = curve_points[path_ids]
# Process curve
# Convert to VTK for visualization
curve_polydata = curves_to_polydata(paths, curve_points)
Isobands
Source: vtk/isobands.py
Single viewport showing the original mesh (semi-transparent), isoband curves, and filled isoband polygons. Hold Ctrl and scroll to move the isoband levels, press n to randomize the cutting plane.
python isobands.py mesh.stl
Features Showcased
- Scalar field from plane distance with
tf.distance - Isoband extraction with
tf.isobands - Boundary curve extraction
Isoband Extraction
import trueform as tf
import numpy as np
center = mesh.points.mean(axis=0)
normal = np.array([1.0, 2.0, 1.0], dtype=mesh.dtype)
normal /= np.linalg.norm(normal)
plane = tf.Plane.from_point_normal(center, normal)
scalars = tf.distance(tf.Point(mesh.points), plane)
cut_values = np.array([-1.0, -0.5, 0.0, 0.5, 1.0], dtype=np.float32)
selected_bands = np.array([0, 2, 4], dtype=np.int32)
(band_faces, band_points), labels, (paths, curve_points) = tf.isobands(
mesh, scalars, cut_values, selected_bands, return_curves=True
)
band_polydata = numpy_to_polydata(band_points, band_faces)
curve_polydata = curves_to_polydata(paths, curve_points)
Isocontours
Source: vtk/isocontours.py
Extracts contour curves at specific threshold values.
python isocontours.py mesh.stl
Isocontour Extraction
import trueform as tf
import numpy as np
# Define scalar field
plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 0])
scalars = tf.distance(tf.Point(mesh.points), plane)
# Single threshold
paths, curve_points = tf.isocontours(mesh, scalars, 0.0)
# Multiple thresholds
thresholds = np.array([-0.5, 0.0, 0.5], dtype=np.float32)
paths, curve_points = tf.isocontours(mesh, scalars, thresholds)
# Visualize as tubes
curve_polydata = curves_to_polydata(paths, curve_points)
tubes = vtk.vtkTubeFilter()
tubes.SetInputData(curve_polydata)
tubes.SetRadius(0.05)
tubes.SetNumberOfSides(20)
OffsetBlockedArray returned by isocontours uses the same offset/data format as VTK cell arrays, enabling efficient conversion.Cross-Section
Source: vtk/cross_section.py
Single viewport showing the original mesh (semi-transparent), cross-section curves, and filled cross-section polygons. Hold Ctrl and scroll to move the cutting plane, press n to randomize.
python cross_section.py mesh.stl
Features Showcased
- Scalar field from plane distance with
tf.distance - Isocontour extraction with
tf.isocontours - Curve triangulation with
tf.triangulated
Cross-Section Extraction
import trueform as tf
import numpy as np
center = mesh.points.mean(axis=0)
normal = np.array([1.0, 2.0, 1.0], dtype=mesh.dtype)
normal /= np.linalg.norm(normal)
plane = tf.Plane.from_point_normal(center, normal)
scalars = tf.distance(tf.Point(mesh.points), plane)
paths, curve_points = tf.isocontours(mesh, scalars, cut_value)
tri_faces, tri_points = tf.triangulated((paths, curve_points))
fill_polydata = numpy_to_polydata(tri_points, tri_faces)
curve_polydata = curves_to_polydata(paths, curve_points)
Interactive Mesh Registration
Source: vtk/alignment.py
Interactive alignment demo with OBB coarse alignment followed by point-to-plane ICP refinement. Drag the source mesh to misalign it, right-drag to rotate, then press 'A' to align.
python alignment.py
Features Showcased
- OBB alignment with
tf.fit_obb_alignment - Point-to-plane ICP with
tf.fit_icp_alignment - Taubin smoothing with
tf.taubin_smoothed - Point normals with
tf.point_normals - Real-time chamfer error with subsampling
Alignment Pipeline
import trueform as tf
# Compute target normals for point-to-plane ICP
target_normals = tf.point_normals(target_mesh)
# OBB coarse alignment
T_obb_delta = tf.fit_obb_alignment(source_cloud, target_cloud)
T_obb = T_obb_delta @ T_source
source_cloud.transformation = T_obb
# ICP refinement (point-to-plane with target normals)
T_icp_delta = tf.fit_icp_alignment(
source_cloud,
(target_cloud, target_normals),
max_iterations=50,
n_samples=1000,
k=1,
)
T_final = T_icp_delta @ T_obb
Real-time Chamfer Error
def update_chamfer(self):
"""Update chamfer error display (subsampled for speed)"""
source_pts = self.source_mesh.points[::10]
T = self.source_cloud.transformation
error = tf.chamfer_error((source_pts, T), self.target_cloud)
self.chamfer_text.SetInput(f"Chamfer error: {error:.4f}")
(points[::10], transformation) to chamfer_error enables fast subsampled error estimation for interactive feedback.Remeshing Pipeline
Source: vtk/remeshing.py
Split-view showing the original mesh (left), decimated result (top-right), and isotropic remeshed result (bottom-right). Edge wireframe on the right panels scales with window height.
python remeshing.py [mesh.stl] [-t 0.05]
Features Showcased
- Decimation with
tf.decimated - Mean edge length with
tf.mean_edge_length - Isotropic remeshing with
tf.isotropic_remeshed - Adaptive edge wireframe scaling
Remesh Pipeline
import trueform as tf
# Load and decimate
faces, points = tf.read_stl("model.stl")
mesh = tf.Mesh(faces, points)
dec_faces, dec_points = tf.decimated(mesh, 0.05)
# Isotropic remesh to mean edge length of decimated result
mel = tf.mean_edge_length((dec_faces, dec_points))
dec_mesh = tf.Mesh(dec_faces, dec_points)
rem_faces, rem_points = tf.isotropic_remeshed(dec_mesh, mel, use_quadric=True)
Adaptive Edge Wireframe
BASE_HEIGHT = 700
def update_edge_width(actors, window):
h = window.GetSize()[1]
w = max(0.5, 0.7 * h / BASE_HEIGHT)
for a in actors:
a.GetProperty().SetLineWidth(w)
# Scale on resize
window.AddObserver("ModifiedEvent",
lambda obj, ev: update_edge_width(edge_actors, obj))
