trimesh icon indicating copy to clipboard operation
trimesh copied to clipboard

Add angled sidewall features for extrude_triangulation

Open gronniger opened this issue 7 months ago • 1 comments

This PR allows to extrude Polygons with some sidewall angle (other than 90°). For 90° (default) behaves exactly as before, for any other angle it calculates the shifted vertices in the extruded face and uses the previous logic as much as possible.

It is mostly motivated by integrating into GDSFactory for rendering waveguides with angled sidewalls.

gronniger avatar Apr 22 '25 15:04 gronniger

Awesome thanks for the PR! A few quick notes:

  • Can we change the parameter from sidewall_angle: float -> taper: float | None?
    • Angle should be in radians rather than degrees
    • Making it None-able avoids the == 90.0 check which probably would have to be changed to np.isclose(taper_angle, np.pi / 2.0), but if taper is None is preferable.
  • it looks like interiors aren't handled? I was seeing Failed with index -4592643848549385702 is out of bounds for axis 0 with size 112 in the quick test below. We probably have to handle those.
  • How does this deal with self-intersections? Not handling these is fine, we should just probably note it in the docstring. If we did want to support that nicely the easiest way to handle them would probably be to do this as shapely.Polygon.buffer operations as then they handle the "did an interior disappear at a particular inflation level" problem and this function would have to match the inflated edges with the original edges.

Quick test:

import numpy as np
from shapely.geometry import Point

import trimesh

if __name__ == "__main__":
    polygons = [
        trimesh.load_path("models/2D/wrench.dxf").polygons_full[0],
        Point(10.0, 10.0).buffer(5.0),
    ]

    for polygon in polygons:
        # (2,) axis aligned bounding box size
        extents = np.ptp(np.reshape(polygon.bounds, (2, 2)), axis=0)

        # try at different heights relative to the polygon scale
        for height in np.linspace(extents.min() / 10.0, extents.max() * 10.0, 10):
            # try at different angles
            for angle in np.linspace(50.0, 140.0, 20):
                print(f"attempting: {height}, {angle}")
                try:
                    extrusion = trimesh.creation.extrude_polygon(
                        polygon, height=height, sidewall_angle=angle
                    )
                except BaseException as E:
                    print(f"Failed with {E}")

mikedh avatar Apr 22 '25 19:04 mikedh

I think this is a bit more complicated to handle for general inputs, particularly if the taper results in topology changes, e.g:

  • taper is positive and there are interiors that may be closed
  • taper is positive and exterior has locally non-convex sections that close
  • taper is positive and exterior has locally convex sections that are properly inflated to a radius around the vertex
  • negative taper

I was discussing this with a friend and we thought that for positive taper "union of cones at each vertex and wedges on every edge for every interior and exterior" gave you the correct answer, although it's kinda slow. A quick prototype gave pretty good results on a "circle with a concentric interior." For negative taper it might be just original.difference(wedges) although I wouldn't swear to it haha. It would also be nice if there was some way of doing this more cheaply!

I'm going to close this as I think we need to handle and test topology changes before merging but open to the feature!

mikedh avatar Aug 11 '25 17:08 mikedh