claude-resources/skills/mesh3d/SKILL.md
Create and edit 3D printable mesh files (STL, 3MF, OBJ) programmatically using Python trimesh + manifold3d. Use when the user asks to create 3D objects, modify existing STL/3MF files, add holes/features to meshes, combine or split meshes, repair meshes, convert between formats, analyze mesh geometry, or generate parametric parts for 3D printing. Triggers on "create STL", "make a 3D model", "edit mesh", "add hole", "modify STL", "3MF", "printable", "mesh repair", "boolean", "combine meshes", or any 3D modeling task that doesn't require a GUI CAD tool.
npx skillsauth add festion/homelab-gitops mesh3dInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Create and edit 3D printable mesh files using Python. Uses trimesh for geometry and I/O, manifold3d for boolean operations, and shapely for 2D profiles.
pip install trimesh manifold3d numpy scipy lxml shapely --break-system-packages
All packages install cleanly from PyPI on Linux x86_64.
import trimesh
import numpy as np
# Create geometry
box = trimesh.creation.box(extents=[width, depth, height])
# Modify
hole = trimesh.creation.cylinder(radius=r, height=h+1)
hole.apply_translation([x, y, 0])
result = box.difference(hole)
# Validate for printing
assert result.is_watertight, "Mesh must be watertight for printing"
# Export
result.export("part.stl") # Binary STL (universal)
result.export("part.3mf") # 3MF (preferred, smaller, metadata)
| Function | Use Case | Key Parameters |
|----------|----------|----------------|
| box(extents=[x,y,z]) | Rectangular solid | extents, transform |
| cylinder(radius, height) | Round post, hole tool | radius, height, sections (smoothness) |
| cone(radius, height) | Tapered shape | radius, height, sections |
| capsule(radius, height) | Rounded cylinder | radius, height |
| uv_sphere(radius) | Ball, dome (cut in half) | radius, count=[lat,lon] |
| annulus(r_min, r_max, height) | Washer, ring, bushing | r_min, r_max, height |
| extrude_polygon(polygon, height) | Custom 2D → 3D shape | Shapely Polygon, height |
| revolve(linestring, sections) | Vase, funnel, knob | Nx2 array of XZ profile points |
| sweep_polygon(polygon, path) | Channel, rail, pipe | Shapely Polygon, Nx3 path points |
Curved primitives use sections parameter (default 32). For 3D printing:
sections=16sections=32sections=64sections=128Spheres use count=[lat, lon] - default [32, 64].
Requires manifold3d. All inputs MUST be watertight.
# Subtract: Remove material (holes, cutouts, pockets)
result = base.difference(tool)
result = base.difference([tool1, tool2, tool3]) # Multiple at once
# Add: Combine shapes
result = part_a.union(part_b)
result = part_a.union([part_b, part_c])
# Intersect: Keep only overlap
result = shape_a.intersection(shape_b)
Critical rules for booleans:
mesh.is_watertight == True)check_volume=False if you get errors on valid geometryresult.is_watertight# Move
mesh.apply_translation([x, y, z])
# Rotate (angle in radians, around axis)
import trimesh.transformations as tf
mesh.apply_transform(tf.rotation_matrix(np.radians(45), [0, 0, 1])) # 45° around Z
# Scale uniformly
mesh.apply_scale(2.0)
# Scale non-uniformly
mesh.apply_scale([1.0, 1.0, 0.5]) # Squish Z to half
# Mirror across plane (e.g., YZ plane → flip X)
mesh.vertices[:, 0] *= -1
mesh.fix_normals()
# Center on origin
mesh.apply_translation(-mesh.center_mass)
# Place on build plate (Z=0)
mesh.apply_translation([0, 0, -mesh.bounds[0][2]])
from shapely.geometry import Polygon as ShapelyPolygon
# L-bracket from 2D outline
points = [(0,0), (20,0), (20,5), (5,5), (5,20), (0,20)]
profile = ShapelyPolygon(points)
mesh = trimesh.creation.extrude_polygon(profile, height=3)
# Circle with hole (washer) from 2D
from shapely.geometry import Point
outer = Point(0, 0).buffer(12) # circle r=12
inner = Point(0, 0).buffer(5) # circle r=5
washer_2d = outer.difference(inner)
washer = trimesh.creation.extrude_polygon(washer_2d, height=2)
# Rounded rectangle
from shapely.geometry import box as shapely_box
rect = shapely_box(-15, -10, 15, 10).buffer(3, resolution=8) # 3mm corner radius
plate = trimesh.creation.extrude_polygon(rect, height=2)
# Load
mesh = trimesh.load("input.stl")
# For 3MF (returns Scene), extract geometry:
scene = trimesh.load("input.3mf")
mesh = list(scene.geometry.values())[0] # first body
# Analyze
print(f"Dimensions: {mesh.extents}") # [x, y, z] size
print(f"Volume: {mesh.volume:.2f} mm³")
print(f"Watertight: {mesh.is_watertight}")
print(f"Bounds: {mesh.bounds}") # [[min_x,y,z],[max_x,y,z]]
# Add mounting holes
for pos in [(10, 10), (-10, 10), (10, -10), (-10, -10)]:
hole = trimesh.creation.cylinder(radius=1.6, height=mesh.extents[2]+2)
hole.apply_translation([pos[0], pos[1], mesh.center_mass[2]])
mesh = mesh.difference(hole)
# Slice off top (cut at Z=15)
cutter = trimesh.creation.box(extents=[200, 200, 200])
cutter.apply_translation([0, 0, 15 + 100]) # box starts at Z=15
mesh = mesh.difference(cutter)
# Save
mesh.export("output.stl")
mesh.export("output.3mf")
# Print readiness check
def check_printability(mesh):
issues = []
if not mesh.is_watertight:
issues.append("Not watertight (has holes)")
if not mesh.is_winding_consistent:
issues.append("Inconsistent face normals")
if mesh.volume < 0:
issues.append("Inverted normals (negative volume)")
mesh.invert() # fix it
if len(mesh.split()) > 1:
issues.append(f"Multiple disconnected bodies ({len(mesh.split())})")
# Check for degenerate faces
degen = trimesh.triangles.area(mesh.triangles) < 1e-8
if degen.any():
issues.append(f"{degen.sum()} degenerate (zero-area) faces")
return issues if issues else ["Ready to print"]
# Mesh info summary
def mesh_info(mesh):
return {
"faces": len(mesh.faces),
"vertices": len(mesh.vertices),
"volume_mm3": round(mesh.volume, 2),
"surface_area_mm2": round(mesh.area, 2),
"dimensions_mm": mesh.extents.round(2).tolist(),
"bounds_mm": mesh.bounds.round(2).tolist(),
"watertight": mesh.is_watertight,
"bodies": len(mesh.split()),
}
# Basic repair pipeline
mesh.merge_vertices() # Remove duplicate vertices
mesh.remove_degenerate_faces() # Remove zero-area faces
mesh.remove_duplicate_faces() # Remove overlapping faces
mesh.fill_holes() # Close small gaps
mesh.fix_normals() # Consistent winding
# If still not watertight, try convex hull as last resort
if not mesh.is_watertight:
# This loses concave detail but guarantees watertight
mesh_fixed = mesh.convex_hull
# STL → 3MF (saves ~50-90% space)
mesh = trimesh.load("model.stl")
mesh.export("model.3mf")
# 3MF → STL
scene = trimesh.load("model.3mf")
for name, geom in scene.geometry.items():
geom.export(f"{name}.stl")
# Any supported format
mesh.export("model.ply") # With vertex colors
mesh.export("model.obj") # ASCII, widely compatible
mesh.export("model.glb") # Binary glTF, modern
See references/printing-patterns.md for detailed examples including:
When creating parts that fit together or onto hardware:
bracket_30x20x3_m3_holes.stlreferences/printing-patterns.md - Common parametric part recipesreferences/api-quick-ref.md - Trimesh API cheat sheettools
TP-Link Omada SDN network management for the Lakehouse homelab. Use when managing switches, access points, VLANs, spanning tree, or WiFi infrastructure. Triggers on phrases like "adopt device", "configure VLAN", "switch CLI", "WiFi troubleshooting", "Omada API", "STP blocking", "PoE budget", or any network hardware management.
devops
Standardized deployment procedure for services in LXC containers on the Proxmox HA cluster. Use when deploying a new service, creating LXC containers, configuring DHCP reservations (Kea), DNS rewrites (AdGuard), reverse proxy routes (Traefik), or integrating with monitoring (Uptime Kuma) and dashboard (Homepage). Triggers on phrases like "deploy a new service", "create container for", "set up DHCP", "add Traefik route", "configure DNS rewrite", or any homelab infrastructure deployment.
testing
Print settings and optimization for Prusa Mini+ with E3D Revo Micro hotend, bowden direct drive extruder, and ObXidian nozzles (0.25mm, 0.4mm, 0.6mm, 0.8mm). Use when the user asks about print settings, slicer profiles, troubleshooting prints, material temperatures, flow rates, nozzle selection, or optimizing prints for this specific printer configuration. Triggers on phrases like "print settings", "slicer profile", "layer height", "print speed", "temperature", "first layer", "stringing", "calibration", "which nozzle", or any 3D printing task mentioning Prusa Mini, Revo, or ObXidian.
data-ai
Use when working with 3D model files (STL), print tracking, slicer profiles, or managing the model-catalog homelab service at model-catalog.internal.lakehouse.wtf