diff --git a/README.md b/README.md index 3b25926..362b1e3 100644 --- a/README.md +++ b/README.md @@ -2,57 +2,73 @@ A Python OOP precompiler for OpenSCAD's language -OpenSCAD ( https://openscad.org/ ) uses a functional scripting language to define solid 3D CAD models. +[OpenSCAD](https://openscad.org/) uses a functional scripting language to define solid 3D CAD models. As such, it is a prefix language (modifiers go before the things they modify). OpenSCADPy allows one to write OpenSCAD scripts using an object representation, which uses method calls to describe modifications. This way, modifications are written after the objects they modify in a postfix fashion, more closely resembling a procedural ordering of steps in the creation of the models. -It also contains convenience functions to define a wider range of primitives, as well as some vector operations. + +It also contains convenience functions to define a wider range of primitives, vector operations, and a method +to export polyhedra directly to STL. ## Example -``` -Cube([1, 1, 2]).move([0, 0, 1]).render() +```python +# example.py +from openscad_py import Cube + +colored_moved_cube = Cube([1, 1, 1]).move([2, 0, 0]).color(r=1, g=0, b=0) +print(colored_moved_cube.render()) ``` -returns - +prints the OpenSCAD code ``` -translate(v=[0, 0, 1]) { cube(size=[1, 1, 2], center=false); } +# example.scad +color(c=[1,0,0,1.0]){ translate(v=[2.0,0.0,0.0]){ +cube(size=[1.0,1.0,1.0], center=false); +} } +``` + +An easy way to write and render the OpenSCAD code would be + +``` +$ python3 example.py > example.scad +$ openscad example.scad ``` ## 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. +Usual computational geometry functions in the `Point` class that work in an arbitrary number of dimensions. +Overloads for algebraic operators. Examples: -``` +```python 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 +`Cylinder.from_ends()` constructs a cylinder between two given points in space. Example: -``` +```python openscad_code = Cylinder.from_ends(radius=2, p1=(0, 0, 1), p2=(1, 0, 2)).render() ``` ### Tubes and toroids from a point grid -`Polyhedron.tube()` creates a tube-like polyhedron from a 2D array of points +`Polyhedron.tube()` creates a tube-like Polyhedron object from a 2D array of points. -`Polyhedron.torus()` creates a toroid polyhedron from a 2D array of points +`Polyhedron.torus()` creates a toroid Polyhedron object from a 2D array of points. ### Tubes from a path -`PathTube` creates a tube-like or toroid polyhedron from an arbitrary path +`PathTube` creates a tube-like or toroid Polyhedron object from an arbitrary path. Example: -``` +```python PathTube( points=[(0,0,0), (0,0,1), (1,0,2), (1,1,0), (0,.5,0)], radius=.2, @@ -64,7 +80,9 @@ PathTube( ### Polyhedron from a height map -``` +`Polyhedron.from_heightmap()` creates a Polyhedron object from a 2D matrix of heights. Example: + +```python Polyhedron.from_heightmap( heights=[ [3, 3, 1, 1, 1], @@ -81,4 +99,24 @@ Polyhedron.from_heightmap( ### Direct STL export -`Polyhedron.render_stl()` exports any polyhedron into STL directly +`Polyhedron.render_stl()` exports a Polyhedron object into STL directly. +This works well with `tube()`, `torus()`, `from_heightmap()` and `PathTube` described above. +Note that the polyhedron object cannot be post-modified (e.g. by `union`, `difference`) - if so, +use OpenSCAD to render the object and export to STL. + +## Overview and usage + +In `openscad_py`, all objects (including derived ones) come with a large set of convenience methods +to apply transformations, implemented in the base [`Object` class](https://github.com/csirmaz/openscad-py/blob/main/openscad_py/object_.py). +This allows to freely specify transformations on any object: + +```python +moved_cube = Cube([1, 1, 1]).move([2, 0, 0]) +colored_moved_cube = Cube([1, 1, 1]).move([2, 0, 0]).color(r=1, g=0, b=0) +``` + +Once the desired object has been created, call `render()` on the final object to obtain the +OpenSCAD code. + +For the full list of convenience functions, see +https://github.com/csirmaz/openscad-py/blob/main/openscad_py/object_.py . diff --git a/example.py b/example.py new file mode 100644 index 0000000..952d4f7 --- /dev/null +++ b/example.py @@ -0,0 +1,7 @@ + +from openscad_py import Cube + +colored_moved_cube = Cube([1, 1, 1]).move([2, 0, 0]).color(r=1, g=0, b=0) +print(colored_moved_cube.render()) + + diff --git a/example.scad b/example.scad new file mode 100644 index 0000000..e2dc851 --- /dev/null +++ b/example.scad @@ -0,0 +1,3 @@ +color(c=[1,0,0,1.0]){ translate(v=[2.0,0.0,0.0]){ +cube(size=[1.0,1.0,1.0], center=false); +} } diff --git a/openscad_py/circle.py b/openscad_py/circle.py index 430bbce..829e94b 100644 --- a/openscad_py/circle.py +++ b/openscad_py/circle.py @@ -29,6 +29,7 @@ class Circle(Object): return cls(r=r, fn=sides) def render(self) -> str: + """Render the object into OpenSCAD code""" fnstr = '' if self.fn is None else f", $fn={self.fn}" return f"circle(r={self.r}{fnstr});" diff --git a/openscad_py/collection.py b/openscad_py/collection.py index 94b9db2..b073466 100644 --- a/openscad_py/collection.py +++ b/openscad_py/collection.py @@ -22,5 +22,6 @@ class Collection(Object): return self.__class__(self.collection + [obj]) def render(self) -> str: + """Render the object into OpenSCAD code""" return "\n".join([o.render() for o in self.collection]) diff --git a/openscad_py/color.py b/openscad_py/color.py index 83b93b0..c7e041e 100644 --- a/openscad_py/color.py +++ b/openscad_py/color.py @@ -13,6 +13,7 @@ class Color(Object): self.child = child def render(self) -> str: + """Render the object into OpenSCAD code""" return f"color(c=[{','.join([str(c) for c in self.color])}]){{ {self.child.render()} }}" diff --git a/openscad_py/cube.py b/openscad_py/cube.py index b9b3560..91a49d9 100644 --- a/openscad_py/cube.py +++ b/openscad_py/cube.py @@ -20,5 +20,6 @@ class Cube(Object): self.center = center def render(self): + """Render the object into OpenSCAD code""" return f"cube(size={self.size.render()}, center={self._center()});" diff --git a/openscad_py/cylinder.py b/openscad_py/cylinder.py index 77c6f66..d148e6a 100644 --- a/openscad_py/cylinder.py +++ b/openscad_py/cylinder.py @@ -22,6 +22,7 @@ class Cylinder(Object): # $fa, $fs, $fn def render(self): + """Render the object into OpenSCAD code""" return f"cylinder(h={self.height}, r1={self.r1}, r2={self.r2}, center={self._center()});" @classmethod diff --git a/openscad_py/delta_offset.py b/openscad_py/delta_offset.py index 1634444..9a0bcc8 100644 --- a/openscad_py/delta_offset.py +++ b/openscad_py/delta_offset.py @@ -19,5 +19,6 @@ class DeltaOffset(Object): self.chamfer = chamfer def render(self) -> str: + """Render the object into OpenSCAD code""" return f"offset(delta={delta}, chamfer={'true' if self.chamfer else 'false'}){{\n{self.child.render()}\n}}" diff --git a/openscad_py/difference.py b/openscad_py/difference.py index b59f689..7a300ce 100644 --- a/openscad_py/difference.py +++ b/openscad_py/difference.py @@ -15,5 +15,6 @@ class Difference(Object): self.tool = Collection.c(tool) # what to remove def render(self) -> str: + """Render the object into OpenSCAD code""" return f"difference(){{ {self.subject.render()}\n{self.tool.render()} }}" diff --git a/openscad_py/header.py b/openscad_py/header.py index bcd7e90..19bd5a4 100644 --- a/openscad_py/header.py +++ b/openscad_py/header.py @@ -8,11 +8,12 @@ class Header: def __init__(self, quality: str = 'draft'): + # See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features#Circle_resolution:_$fa,_$fs,_and_$fn self.quality = quality def render(self): - # See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features#Circle_resolution:_$fa,_$fs,_and_$fn + """Return OpenSCAD code""" if self.quality == 'draft': return "" if self.quality == 'mid': diff --git a/openscad_py/intersection.py b/openscad_py/intersection.py index c743149..6397a85 100644 --- a/openscad_py/intersection.py +++ b/openscad_py/intersection.py @@ -14,6 +14,7 @@ class Intersection(Object): self.child = Collection.c(child) def render(self) -> str: + """Render the object into OpenSCAD code""" return f"intersection(){{ {self.child.render()} }}" diff --git a/openscad_py/linear_extrude.py b/openscad_py/linear_extrude.py index 2d50dd1..3a7b6d3 100644 --- a/openscad_py/linear_extrude.py +++ b/openscad_py/linear_extrude.py @@ -20,6 +20,7 @@ class LinearExtrude(Object): # twist, slices, scale (float/vector), $fn def render(self) -> str: + """Render the object into OpenSCAD code""" return f"linear_extrude(height={self.height}, center={self._center()}, convexity={self.convexity}){{\n{self.child.render()}\n}}" diff --git a/openscad_py/object_.py b/openscad_py/object_.py index 2f69f85..4f8b825 100644 --- a/openscad_py/object_.py +++ b/openscad_py/object_.py @@ -18,6 +18,7 @@ class Object: return Collection([self, obj]) def render(self) -> str: + """Render the object into OpenSCAD code""" raise Exception("abstract method") def translate(self, v: TUnion[list, Point]) -> 'Object': diff --git a/openscad_py/path_tube.py b/openscad_py/path_tube.py index 361fffb..b1d3fe2 100644 --- a/openscad_py/path_tube.py +++ b/openscad_py/path_tube.py @@ -119,6 +119,7 @@ class PathTube(Object): return Polyhedron.tube(points=points_rows, convexity=self.convexity, make_torus=self.make_torus) def render(self) -> str: + """Render the object into OpenSCAD code""" return self.process().render() diff --git a/openscad_py/point.py b/openscad_py/point.py index e4f699b..79e5452 100644 --- a/openscad_py/point.py +++ b/openscad_py/point.py @@ -22,7 +22,7 @@ class Point: return Point(coords) def render(self) -> str: - """Render the point into a SCAD script""" + """Render the object into OpenSCAD code""" return "[" + (",".join([str(c) for c in self.c])) + "]" def render_stl(self) -> str: diff --git a/openscad_py/polygon.py b/openscad_py/polygon.py index 9181886..3b49bcb 100644 --- a/openscad_py/polygon.py +++ b/openscad_py/polygon.py @@ -16,5 +16,6 @@ class Polygon(Object): self.convexity = convexity def render(self) -> str: + """Render the object into OpenSCAD code""" return f"polygon(points=[{','.join([p.render() for p in self.points])}], convexity={self.convexity});" diff --git a/openscad_py/polyhedron.py b/openscad_py/polyhedron.py index ae484f3..80e1c62 100644 --- a/openscad_py/polyhedron.py +++ b/openscad_py/polyhedron.py @@ -176,6 +176,7 @@ class Polyhedron(Object): return cls(points=point_list, faces=faces, convexity=convexity) def render(self) -> str: + """Render the object into OpenSCAD code""" faces_list = [f"[{','.join([str(x) for x in face])}]" for face in self.faces] return f"polyhedron(points=[{','.join([p.render() for p in self.points])}], faces=[{','.join(faces_list)}], convexity={self.convexity});" diff --git a/openscad_py/radial_offset.py b/openscad_py/radial_offset.py index 3de2d29..07b7d0d 100644 --- a/openscad_py/radial_offset.py +++ b/openscad_py/radial_offset.py @@ -19,6 +19,7 @@ class RadialOffset(Object): # $fa, $fs, and $fn def render(self) -> str: + """Render the object into OpenSCAD code""" return f"offset(r={self.r}){{\n{self.child.render()}\n}}" diff --git a/openscad_py/rotate.py b/openscad_py/rotate.py index 585d7cb..76a4744 100644 --- a/openscad_py/rotate.py +++ b/openscad_py/rotate.py @@ -16,6 +16,7 @@ class Rotate(Object): self.child = child def render(self) -> str: + """Render the object into OpenSCAD code""" return f"rotate(a={self.a}, v={self.v.render()}){{\n{self.child.render()}\n}}" diff --git a/openscad_py/rotate_extrude.py b/openscad_py/rotate_extrude.py index 8b80e95..19ed745 100644 --- a/openscad_py/rotate_extrude.py +++ b/openscad_py/rotate_extrude.py @@ -21,6 +21,7 @@ class RotateExtrude(Object): # $fa, $fs, $fn def render(self) -> str: + """Render the object into OpenSCAD code""" return f"rotate_extrude(angle={self.angle}, convexity={self.convexity}) {{\n{self.child.render()}\n}}" diff --git a/openscad_py/scale.py b/openscad_py/scale.py index 619de28..42533e1 100644 --- a/openscad_py/scale.py +++ b/openscad_py/scale.py @@ -16,6 +16,7 @@ class Scale(Object): self.child = child def render(self) -> str: + """Render the object into OpenSCAD code""" return f"scale(v={self.v.render()}){{\n{self.child.render()}\n}}" diff --git a/openscad_py/sphere.py b/openscad_py/sphere.py index e287e32..b0df00b 100644 --- a/openscad_py/sphere.py +++ b/openscad_py/sphere.py @@ -14,13 +14,12 @@ class Sphere(Object): See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/The_OpenSCAD_Language#sphere """ - def __init__(self, r): self.r = r # $fa, $fs, $fn - def render(self): + """Render the object into OpenSCAD code""" return f"sphere(r={self.r});" diff --git a/openscad_py/translate.py b/openscad_py/translate.py index a58d9b5..c18c1da 100644 --- a/openscad_py/translate.py +++ b/openscad_py/translate.py @@ -15,5 +15,6 @@ class Translate(Object): self.child = child def render(self) -> str: + """Render the object into OpenSCAD code""" return f"translate(v={self.v.render()}){{\n{self.child.render()}\n}}" diff --git a/openscad_py/union.py b/openscad_py/union.py index de7f01a..e3aa067 100644 --- a/openscad_py/union.py +++ b/openscad_py/union.py @@ -15,6 +15,7 @@ class Union(Object): self.child = Collection.c(child) def render(self) -> str: + """Render the object into OpenSCAD code""" return f"union(){{ {self.child.render()} }}" def union(self, objects: TUnion[list, Object]) -> Object: