Last week I published SupportSage, a tool that analyzes STL geometry and generates optimized supports. The response was immediate: "Tree supports already exist in Cura and PrusaSlicer — what's different about yours?"
Fair question. The answer is: existing tools use rule-based tree generation. SupportSage uses geometry-aware optimization. Let me show you exactly what that means, with the actual code.
The Problem with Existing Tree Supports
Both Cura and PrusaSlicer have "organic" or "tree" support modes. They're better than uniform block supports, but they share a fundamental limitation: they apply the same branching algorithm globally. Every overhang region gets the same treatment.
What I found is that overhangs fall into three distinct categories that need fundamentally different support strategies:
| Type | Angle from Vertical | Support Strategy | Material Saved vs Uniform |
|---|---|---|---|
| Critical | >120° (nearly horizontal) | Dense interface + thick branches | 15-25% |
| Moderate | 70-120° | Tree supports, medium density | 35-45% |
| Borderline | 45-70° | Light touch, thin pillars | 50-65% |
| Safe | <45° | No support needed | 100% |
The insight: treating all overhangs the same is the waste. A face at 135° needs a dense foundation. A face at 50° needs a light tap. Existing slicers can't distinguish.
Step 1: Geometry Analysis
Everything starts with the mesh. I use trimesh to load the STL, then analyze each face normal against the build direction (Z-up):
# face_normals: (N, 3) array of unit face normals
# Build direction: Z-up (0, 0, 1)
dot_products = face_normals @ np.array([0.0, 0.0, 1.0])
# cos(45°) ≈ 0.707 — faces steeper than 45° need support
cos_threshold = np.cos(np.radians(45.0))
overhang_mask = dot_products < cos_threshold
Simple. But this alone gives false positives. A gentle slope on a benchy hull might have every face at 46°, technically an overhang but structurally stable. The fix: clustering.
# Build adjacency graph of overhang faces
# Two faces are adjacent if they share an edge
for each overhang face:
BFS to collect connected components
→ each connected component is one "island"
A bridge span generates two islands — one at each end — instead of one giant overhang blob. This is where the material savings start.
Step 2: The Branching Algorithm
Here's the core. For each overhang island, I sample support points from the downward-facing surface:
for each island:
for each face in island:
centroid = face.vertices.mean(axis=0)
normal = face.normal
if normal[2] < 0: # faces downward
points.append(centroid - normal * 0.1)
Then, for each support point, I grow a branch downward — not a straight cylinder, but a branching structure that merges with nearby branches:
Support point (top)
|
| Branch segment (angled)
|
/ \ Split point
/ \
| |
| | ← merges with nearby trunk
|
Build plate (z=0)
The merge optimization is the key. When a new branch grows down, it checks if any existing trunk passes within MERGE_DISTANCE (5mm) in XY:
def grow_branch(support_point, existing_trunks):
pos = support_point.pos
# Check merge distance against existing trunks
for trunk in existing_trunks:
for trunk_point in trunk:
dist_xy = distance(pos.xy, trunk_point.xy)
if dist_xy < 5.0 and trunk_point.z < pos.z:
# Merge into this trunk instead of growing to plate
# This saves 30-50% material vs separate pillars
branch_to(trunk_point)
return
# No merge found — grow all the way to build plate
branch_to(build_plate_at_z_0)
Step 3: Angle Constraints for Printability
A branch going straight sideways isn't printable. The maximum overhang angle for the support itself (not the model) is typically 60° from vertical:
MAX_BRANCH_ANGLE = 60 # degrees
delta = parent_pos - child_pos
horizontal_dist = norm(delta.xy)
vertical_dist = abs(delta.z)
if horizontal_dist / vertical_dist > tan(radians(60)):
# Too steep! Add an intermediate node
max_h = vertical_dist * tan(radians(60))
intermediate = child_pos + direction_xy * max_h
intermediate.z = child_pos.z - vertical_dist * 0.5
This creates printable zigzag paths that follow the shortest structurally sound route.
Step 4: Geometry Generation
Each branch segment becomes a tapered cylinder:
p1 (top, wider) —— radius r1
|
| ← tapered
|
p2 (bottom, narrower) —— radius r2
The taper is important — supports that are wider at the top (near the model) and narrower at the bottom save material while maintaining stability where it matters most.
# Generate vertices for bottom and top rings
for angle in linspace(0, 2π, segments, endpoint=False):
circle_vec = right * cos(angle) + forward * sin(angle)
bottom_verts.append(p1 + circle_vec * r1)
top_verts.append(p2 + circle_vec * r2)
# Connect into triangle mesh
for i in range(segments):
next = (i + 1) % segments
faces.append([i, next, i + segments])
faces.append([next, next + segments, i + segments])
The Measurable Difference
I ran comparative tests on three standard overhang models:
| Model | Traditional Support | SupportSage Balanced | Savings |
|---|---|---|---|
| Bridge (30mm span) | ~5,600mm³ | ~3,650mm³ | 35% |
| Multi-island roof | ~6,900mm³ | ~4,480mm³ | 35% |
| Wide canopy | ~2,500mm³ | ~1,620mm³ | 35% |
The savings are remarkably consistent at ~35% because the algorithm doesn't make radical changes — it just stops printing support where it's not needed.
Where It Goes Next
The current tree support geometry is functional but basic. The next iteration adds:
- Load-path optimization — thicker branches under heavier overhangs, thinner under light ones
- Surface-quality prediction — if a support contact point would leave a visible scar on a visible surface, route support elsewhere
- Multi-material awareness — when using soluble filament (PVA, BVOH), optimize branch placement differently
The full code is open source: github.com/bossman-lab/supportsage
git clone https://github.com/bossman-lab/supportsage
cd supportsage && pip install -e .
# See what your model needs
supportsage analyze your_model.stl
# Generate tree-optimized supports
supportsage tree your_model.stl -o optimized.stl --strategy balanced
The biggest surprise in building this: the math is simpler than I expected. The hard part isn't the algorithm — it's deciding to build something better than "good enough."
Built with trimesh, numpy, and the conviction that 35% of the world's 3D printing filament shouldn't end up in the trash.
Top comments (0)