diff --git a/README.md b/README.md index e363faf..3b25926 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ translate(v=[0, 0, 1]) { cube(size=[1, 1, 2], center=false); } ## Notable convenience functions +### Computational geometry + Usual computational geometry functions on the `Point` class that work in an arbitrary number of dimensions. Overloads for algebraic operators. ``` @@ -32,13 +34,21 @@ distance = (Point((0, 0, 1)) - Point((1, 0, 1))).length() angle_between = Point((0, 0, 1) * 2).angle(Point((1, 0, 1))) ``` +### Cylinders + `Cylinder.from_ends()` constructs a cylinder between two given points in space ``` openscad_code = Cylinder.from_ends(radius=2, p1=(0, 0, 1), p2=(1, 0, 2)).render() ``` -`Polyhedron.tube()` creates a tube-like or toroid polyhedron from a 2D array of points +### Tubes and toroids from a point grid + +`Polyhedron.tube()` creates a tube-like polyhedron from a 2D array of points + +`Polyhedron.torus()` creates a toroid polyhedron from a 2D array of points + +### Tubes from a path `PathTube` creates a tube-like or toroid polyhedron from an arbitrary path @@ -52,4 +62,23 @@ PathTube( ![PathTube example](https://raw.github.com/csirmaz/openscad-py/master/images/pathtube.png) -`Polyhedron.render_stl()` export a polyhedra into STL directly +### Polyhedron from a height map + +``` +Polyhedron.from_heightmap( + heights=[ + [3, 3, 1, 1, 1], + [3, 3, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 2, 2], + [1, 1, 1, 2, 2], + ], + base=-5 +) +``` + +![Heightmap example](https://raw.github.com/csirmaz/openscad-py/master/images/heightmap.png) + +### Direct STL export + +`Polyhedron.render_stl()` exports any polyhedron into STL directly diff --git a/images/heightmap.png b/images/heightmap.png new file mode 100644 index 0000000..db22191 Binary files /dev/null and b/images/heightmap.png differ diff --git a/openscad_py.py b/openscad_py.py index 0529af9..26f2ec1 100644 --- a/openscad_py.py +++ b/openscad_py.py @@ -297,6 +297,8 @@ class Cylinder(Object): class Polyhedron(Object): """A 3D primitive, a polyhedron defined by a list of points and faces.""" # See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/The_OpenSCAD_Language#polyhedron + # Faces are defined by lists of point indices. The points of a face must be listed clockwise when + # looking at the face from the outside inward. # Nonplanar faces should be triangulated by opensCAD def __init__(self, points: List[TUnion[list, Point]], faces: List[list], convexity: int = 10): @@ -304,10 +306,11 @@ class Polyhedron(Object): self.faces = faces self.convexity = convexity + @classmethod def torus(cls, points: List[List[TUnion[list, Point]]], torus_connect_offset: int = 0, convexity: int = 10): """Construct a torus-like polyhedron from a 2D array of points. - Each row of points must be oriented clickwise when looking from the first row (loop) toward the next. + Each row of points must be oriented clockwise when looking from the first row (loop) toward the next. The rows of points form loops. points: A 2D array of points @@ -316,6 +319,7 @@ class Polyhedron(Object): """ return cls.tube(points=points, convexity=convexity, make_torus=True, torus_connect_offset=torus_connect_offset) + @classmethod def tube(cls, points: List[List[TUnion[list, Point]]], make_torus: bool = False, torus_connect_offset: int = 0, convexity: int = 10): """Construct a tube-like polyhedron from a 2D array of points. @@ -373,6 +377,91 @@ class Polyhedron(Object): ]) return cls(points=point_list, faces=faces, convexity=convexity) + + @classmethod + def from_heightmap(cls, heights: List[List[float]], base: float = 0., convexity: int = 10): + """Construct a polyhedron from a 2D matrix of heights. If the height at [0,0] is Z, it maps + to the point (0, 0, Z). + + heights: The 2D matrix of heights + base: The height at which the base will be - in the scale of heights (optional; default 0) + convexity: see OpenSCAD + """ + rows = len(heights) + row_len = len(heights[0]) + point_list = [] + point_map = {} # { (row_ix,col_ix) -> list_ix, ... + bottom_point_map = {} + for row_ix, row in enumerate(heights): + for col_ix, height in enumerate(row): + point = Point([row_ix, col_ix, height]) + bottom_point = Point([row_ix, col_ix, base]) + + point_map[(row_ix, col_ix)] = len(point_list) + point_list.append(point) + + bottom_point_map[(row_ix, col_ix)] = len(point_list) + point_list.append(bottom_point) + + faces = [] + + # Surface (top) faces + # r 10 11 + # c + # 10 1 2 + # 11 4 3 + for row_ix in range(1, rows): + for col_ix in range(1, row_len): + faces.append([ + point_map[(row_ix-1, col_ix-1)], + point_map[(row_ix, col_ix-1)], + point_map[(row_ix, col_ix)], + point_map[(row_ix-1, col_ix)] + ]) + + # Bottom faces + for row_ix in range(1, rows): + for col_ix in range(1, row_len): + faces.append([ + bottom_point_map[(row_ix-1, col_ix-1)], # 1 + bottom_point_map[(row_ix-1, col_ix)], # 4 + bottom_point_map[(row_ix, col_ix)], # 3 + bottom_point_map[(row_ix, col_ix-1)] # 2 + ]) + + # Side faces + for row_ix in range(1, rows): + m = row_len - 1 + faces.append([ + point_map[(row_ix-1, m)], + point_map[(row_ix, m)], + bottom_point_map[(row_ix, m)], + bottom_point_map[(row_ix-1, m)] + ]) + faces.append([ + point_map[(row_ix, 0)], + point_map[(row_ix-1, 0)], + bottom_point_map[(row_ix-1, 0)], + bottom_point_map[(row_ix, 0)] + ]) + + for col_ix in range(1, row_len): + m = rows - 1 + faces.append([ + point_map[(m, col_ix-1)], + point_map[(m, col_ix)], + bottom_point_map[(m, col_ix)], + bottom_point_map[(m, col_ix-1)] + ]) + faces.append([ + point_map[(0, col_ix)], + point_map[(0, col_ix-1)], + bottom_point_map[(0, col_ix-1)], + bottom_point_map[(0, col_ix)] + ]) + + return cls(points=point_list, faces=faces, convexity=convexity) + def render(self) -> str: faces_list = [f"[{','.join([str(x) for x in face])}]" for face in self.faces]