Add scaffolding generation for overhangs

This commit is contained in:
Richard Ward
2025-07-18 13:05:43 +01:00
parent ba730fcaba
commit 6f1eaf283d
4 changed files with 99 additions and 0 deletions

View File

@@ -55,6 +55,21 @@ unioned = combine(boxes)
result = safe_difference(unioned, Cylinder(0.5, 1).mesh())
```
## Overhang Scaffolding
`generate_scaffolding` creates simple cylindrical supports beneath
downward facing surfaces that exceed a chosen overhang angle. The supports
are meant to be easy to remove after printing.
```python
from parametric_cad import generate_scaffolding, Box, combine
base = Box(20, 20, 10)
ledge = Box(10, 10, 5).at(15, 5, 10)
model = combine([base, ledge])
supports = generate_scaffolding(model)
```
## License
This project is licensed under the [MIT License](LICENSE).

View File

@@ -11,6 +11,7 @@ from .primitives.sphere import Sphere
from .mechanisms.butthinge import ButtHinge
from .export.stl import STLExporter
from .printability import PrintabilityValidator
from .scaffolding import generate_scaffolding
__all__ = [
"tm",
@@ -26,6 +27,7 @@ __all__ = [
"ButtHinge",
"STLExporter",
"PrintabilityValidator",
"generate_scaffolding",
"Polygon",
"Point",
"box",

View File

@@ -0,0 +1,68 @@
from __future__ import annotations
import numpy as np
from .core import tm
def generate_scaffolding(
mesh: tm.Trimesh,
*,
max_angle_deg: float = 45.0,
support_radius: float = 0.5,
grid_size: float = 5.0,
sections: int = 8,
) -> tm.Trimesh:
"""Return support scaffolding for downward overhangs of ``mesh``.
Parameters
----------
mesh:
Mesh to analyze for overhangs.
max_angle_deg:
Angles from vertical greater than this require support.
support_radius:
Radius of the cylindrical support columns.
grid_size:
Grid spacing for clustering support columns. ``0`` disables clustering.
sections:
Number of cylinder sections used to generate the columns.
"""
normals = mesh.face_normals
centers = mesh.triangles_center
min_z = float(mesh.bounds[0, 2])
angles = np.degrees(np.arccos(np.clip(normals[:, 2], -1.0, 1.0)))
overhang = (angles > 90.0 + max_angle_deg) & (centers[:, 2] > min_z + 1e-6)
if not np.any(overhang):
return tm.Trimesh()
pts = centers[overhang]
if grid_size > 0:
xy = np.round(pts[:, :2] / grid_size) * grid_size
unique_xy, inverse = np.unique(xy, axis=0, return_inverse=True)
top_z = np.zeros(len(unique_xy))
for i in range(len(unique_xy)):
top_z[i] = pts[inverse == i][:, 2].max()
else:
unique_xy = pts[:, :2]
top_z = pts[:, 2]
supports = []
for (x, y), z in zip(unique_xy, top_z):
height = z - min_z
if height <= 0:
continue
cyl = tm.creation.cylinder(
radius=support_radius, height=height, sections=sections
)
cyl.apply_translation([0, 0, height / 2])
cyl.apply_translation([x, y, min_z])
supports.append(cyl)
if not supports:
return tm.Trimesh()
return tm.util.concatenate(supports)
__all__ = ["generate_scaffolding"]

14
tests/test_scaffolding.py Normal file
View File

@@ -0,0 +1,14 @@
import pytest
from parametric_cad import Box, combine, generate_scaffolding
def test_scaffolding_generation():
base = Box(2.0, 2.0, 1.0)
overhang = Box(1.0, 1.0, 0.5).at(1.5, 0.5, 1.0)
mesh = combine([base, overhang])
scaff = generate_scaffolding(mesh)
assert isinstance(scaff, type(mesh))
# Should extend from model base upwards
assert scaff.bounds[0, 2] == pytest.approx(mesh.bounds[0, 2])
assert scaff.bounds[1, 2] <= mesh.bounds[1, 2]
assert scaff.vertices.shape[0] > 0