From ffbee53da31f89506895bfbfd5279141ec212d13 Mon Sep 17 00:00:00 2001 From: Elod Csirmaz Date: Sat, 30 Nov 2024 17:19:51 +0000 Subject: [PATCH] Add support for polyhedron from height maps --- README.md | 33 +++++++++++++++- images/heightmap.png | Bin 0 -> 9247 bytes openscad_py.py | 91 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 images/heightmap.png 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 0000000000000000000000000000000000000000..db22191b24c2feaabb6a4b9cbf48afc8e8b23c03 GIT binary patch literal 9247 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 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]