diff --git a/openscad_py.py b/openscad_py.py index e3b3513..db69a61 100644 --- a/openscad_py.py +++ b/openscad_py.py @@ -1,5 +1,5 @@ -from typing import Union +from typing import Union as TUnion import math import numpy as np @@ -14,7 +14,7 @@ class Point: self.c = np.array(coords, dtype=NP_TYPE) @classmethod - def c(cls, coords: Union[list, 'Point']) -> 'Point': + def c(cls, coords: TUnion[list, 'Point']) -> 'Point': """Ensure coords is an instance of Point""" if isinstance(coords, Point): return coords @@ -22,7 +22,7 @@ class Point: def render(self) -> str: """Render the point into a SCAD script""" - return ",".join([str(c) for c in self.c]) + return "[" + (",".join([str(c) for c in self.c])) + "]" def scale(self, x: float) -> 'Point': """Scale the current vector/point by a scalar""" @@ -136,36 +136,58 @@ class Object: def _center(self) -> str: return ('true' if self.center else 'false') - def add(self, obj): + def _add(self, obj): + """Add an object, forming a collection""" return Collection([self, obj]) def render(self) -> str: raise Exception("abstract method") - def translate(self, v: Union[list, Point]): + def translate(self, v: TUnion[list, Point]) -> 'Object': """Apply a translation""" return Translate(v=v, child=self) - def move(self, v: Union[list, Point]): + def move(self, v: TUnion[list, Point]) -> 'Object': """Apply a translation""" return Translate(v=v, child=self) - def rotate(self, a, v: Union[list, Point]): + def rotate(self, a, v: TUnion[list, Point]) -> 'Object': """Apply a rotation""" return Rotate(a=a, v=v, child=self) + def scale(self, v: TUnion[list, Point, float]) -> 'Object': + """Apply scaling. Accepts a single float for uniform scaling""" + return Scale(v=v, child=self) + + def color(self, r, g, b, a=1.) -> 'Object': + """Apply a color""" + return Color(r=r, g=g, b=b, a=a, child=self) + + def extrude(self, height, convexity = 10, center: bool = False) -> 'Object': + """Apply a linear extrusion""" + return LinearExtrude(height=height, child=self, convexity=convexity, center=center) + + def diff(self, tool: TUnion[list, 'Object']) -> 'Object': + """Remove from the object using a difference operator""" + return Difference(subject=self, tool=tool) + + def union(self, objects: TUnion[list, 'Object']) -> 'Object': + return Union(child=Collection.c(objects)._add(self)) + class Cube(Object): + """A 3D primitive, cube""" - def __init__(self, size: Union[list, Point], center: bool = False): + def __init__(self, size: TUnion[list, Point], center: bool = False): self.size = Point.c(size) self.center = center def render(self): - return f"cube(size=[{self.size.render()}], center={self._center()});" + return f"cube(size={self.size.render()}, center={self._center()});" class Sphere(Object): + """A 3D primitive, sphere""" def __init__(self, r): self.r = r @@ -176,6 +198,7 @@ class Sphere(Object): class Cylinder(Object): + """A 3D primitive, cylinder""" def __init__(self, h, r=None, r1=None, r2=None, center: bool = False): self.height = h @@ -188,7 +211,7 @@ class Cylinder(Object): return f"cylinder(h={self.height}, r1={self.r1}, r2={self.r2}, center={self._center()});" @classmethod - def from_ends(cls, radius: float, p1: Union[list, Point], p2: Union[list, Point]) -> Object: + def from_ends(cls, radius: float, p1: TUnion[list, Point], p2: TUnion[list, Point]) -> Object: """Construct a cylinder between two points""" p1 = Point.c(p1) p2 = Point.c(p2) @@ -205,9 +228,42 @@ class Cylinder(Object): rangle = 0 r = z return cls(h=length, r=radius, center=False).rotate(a=rangle, v=r).move(p1) - +class Circle(Object): + """A 2D primitive, circle""" + + def __init__(self, r, fn=None): + self.r = r + self.fn = fn + # $fa, $fs, $fn + + @classmethod + def triangle(cls, r): + """Create a regular triangle""" + return cls(r=r, fn=3) + + @classmethod + def regular_polygon(cls, r, sides: int): + """Create a regular polygon""" + return cls(r=r, fn=sides) + + def render(self) -> str: + fnstr = '' if self.fn is None else f", $fn={self.fn}" + return f"circle(r={self.r}{fnstr});" + + +class Polygon(Object): + """A 2D primitive, polygon""" + + def __init__(self, points, paths=None, convexity=1): + assert paths is None # not implemented yet + self.points = [Point.c(p) for p in points] + self.convexity = convexity + + def render(self) -> str: + return f"polygon(points=[{','.join([p.render() for p in self.points])}], convexity={self.convexity});" + # TODO polyhedron(points=[[],], faces[[p,],], convexity=) # TODO https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types @@ -216,35 +272,110 @@ class Collection(Object): def __init__(self, coll: list): self.collection = coll + + @classmethod + def c(cls, coll: TUnion[list, Object]) -> Object: + """Cast lists to collections""" + if isinstance(coll, Object): + return coll + return cls(coll) - def add(self, obj): - self.collection.append(obj) - - def render(self): + def _add(self, obj): + return self.__class__(self.collection + [obj]) + + def render(self) -> str: return "\n".join([o.render() for o in self.collection]) class Translate(Object): """Represents a translation transformation applied to an object""" - def __init__(self, v: Union[list, Point], child: Object): + def __init__(self, v: TUnion[list, Point], child: Object): self.v = Point.c(v) self.child = child - def render(self): - return f"translate(v=[{self.v.render()}]){{\n{self.child.render()}\n}}" + def render(self) -> str: + return f"translate(v={self.v.render()}){{\n{self.child.render()}\n}}" class Rotate(Object): """Represents a rotation transformation applied to an object""" - def __init__(self, a, v: Union[list, Point], child: Object): + def __init__(self, a, v: TUnion[list, Point], child: Object): self.a = a self.v = Point.c(v) self.child = child - def render(self): - return f"rotate(a={self.a}, v=[{self.v.render()}]){{\n{self.child.render()}\n}}" - - - + def render(self) -> str: + return f"rotate(a={self.a}, v={self.v.render()}){{\n{self.child.render()}\n}}" + + +class Scale(Object): + + def __init__(self, v: TUnion[list, Point, float], child: Object): + if isinstance(v, float): + v = [v, v, v] + self.v = Point.c(v) + self.child = child + + def render(self) -> str: + return f"scale(v={self.v.render()}){{\n{self.child.render()}\n}}" + + +class Color(Object): + + def __init__(self, child: Object, r, g, b, a=1.): + self.color = [r, g, b, a] + self.child = child + + def render(self) -> str: + return f"color(c=[{','.join([str(c) for c in self.color])}]){{ {self.child.render()} }}" + + +class LinearExtrude(Object): + """Represents a linear extrusion applied to an object""" + + def __init__(self, height, child: Object, convexity = 10, center: bool = False): + self.height = height + self.child = child + self.convexity = convexity + self.center = center + # twist, slices, scale (float/vector), $fn + + def render(self) -> str: + return f"linear_extrude(height={self.height}, center={self._center()}, convexity={self.convexity}){{\n{self.child.render()}\n}}" + + +class Union(Object): + """Represents a union applied to an object (usually a collection of objects)""" + + def __init__(self, child: TUnion[Object, list]): + self.child = Collection.c(child) + + def render(self) -> str: + return f"union(){{ {self.child.render()} }}" + + def union(self, objects: TUnion[list, Object]) -> Object: + return self.__class__(self.child._add(objects)) + + +class Intersection(Object): + """Represents an intersection applied to an object (usually a collection of objects)""" + + def __init__(self, child: TUnion[Object, list]): + self.child = Collection.c(child) + + def render(self) -> str: + return f"intersection(){{ {self.child.render()} }}" + + +class Difference(Object): + """Represents a difference""" + + def __init__(self, subject: Object, tool: TUnion[list, Object]): + self.subject = subject + self.tool = Collection.c(tool) # what to remove + + def render(self) -> str: + return f"difference(){{ {self.subject.render()}\n{self.tool.render()} }}" +