From 787a3689e030b4f8c91f11a8a9a947ceaa6e57b3 Mon Sep 17 00:00:00 2001 From: Elod Csirmaz Date: Sun, 10 Dec 2023 23:41:28 +0000 Subject: [PATCH] Convenience methods for cylinders --- .gitignore | 4 ++ openscad_py.py | 107 +++++++++++++++++++++++++++++++++++++++---------- test.py | 7 ++++ 3 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 .gitignore create mode 100644 test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6429353 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*~ +*.bak +__pycache__ +.idea \ No newline at end of file diff --git a/openscad_py.py b/openscad_py.py index e00db28..e3b3513 100644 --- a/openscad_py.py +++ b/openscad_py.py @@ -1,5 +1,6 @@ from typing import Union +import math import numpy as np EPSILON = 1e-7 @@ -7,31 +8,32 @@ NP_TYPE = np.float_ class Point: - """Represents a 3D point of vector""" + """Represents a point of vector in arbitrary dimensions""" def __init__(self, coords): self.c = np.array(coords, dtype=NP_TYPE) @classmethod - def c(cls, coords: Union[list, Point]) -> Point: + def c(cls, coords: Union[list, 'Point']) -> 'Point': """Ensure coords is an instance of Point""" if isinstance(coords, Point): return coords return Point(coords) def render(self) -> str: + """Render the point into a SCAD script""" return ",".join([str(c) for c in self.c]) - def scale(self, x: float) -> Point: + def scale(self, x: float) -> 'Point': """Scale the current vector/point by a scalar""" return self.__class__(self.c * x) - def add(self, p: Point) -> Point: + def add(self, p: 'Point') -> 'Point': assert isinstance(p, Point) assert self.dim() == p.dim() return self.__class__(self.c + p.c) - def sub(self, p: Point) -> Point: + def sub(self, p: 'Point') -> 'Point': assert isinstance(p, Point) assert self.dim() == p.dim() return self.__class__(self.c - p.c) @@ -48,34 +50,53 @@ class Point: """Return the length of the vector""" return np.sqrt(np.square(self.c).sum()) - def norm(self) -> Point: + def norm(self) -> 'Point': l = self.length() if l == 0: raise Exception("normalising 0 vector") return self.__class__(self.c / self.length()) - def dot(self, p: Point) -> float: + def dot(self, p: 'Point') -> float: + """Return the dot product""" return np.dot(self.c, p.c) + + def cross(self, p: 'Point') -> 'Point': + """Return the cross product""" + assert self.dim() == 3 + assert p.dim() == 3 + return Point([ + self.c[1]*p.c[2] - self.c[2]*p.c[1], + self.c[2]*p.c[0] - self.c[0]*p.c[2], + self.c[0]*p.c[1] - self.c[1]*p.c[0] + + ]) - def eq(self, p: Point) -> bool: + def eq(self, p: 'Point') -> bool: return (self.c == p.c).all() - def lt(self, p: Point) -> bool: + def lt(self, p: 'Point') -> bool: return (self.c < p.c).all() - def le(self, p: Point) -> bool: + def le(self, p: 'Point') -> bool: return (self.c <= p.c).all() - def gt(self, p: Point) -> bool: + def gt(self, p: 'Point') -> bool: return (self.c > p.c).all() - def ge(self, p: Point) -> bool: + def ge(self, p: 'Point') -> bool: return (self.c >= p.c).all() - def allclose(self, p: Point) -> bool: + def allclose(self, p: 'Point') -> bool: return self.c.shape == p.c.shape and np.allclose(self.c, p.c) + + def angle(self, p: 'Point') -> float: + """Return the angle between two vectors, in degrees""" + r = self.dot(p) + r = r / self.length() / p.length() + r = math.acos(r) + return r / math.pi * 180. - def rotate(self, coords, angle: float) -> Point: + def rotate(self, coords, angle: float) -> 'Point': """Rotate. coords is a list of 2 coordinate indices that we rotate""" assert len(coords) == 2 ca, cb = coords @@ -112,33 +133,39 @@ class Point: class Object: """Abstract class for an SCAD object""" - def __init__(self): - pass - def _center(self) -> str: return ('true' if self.center else 'false') def add(self, obj): - return Collection([self, action]) + return Collection([self, obj]) def render(self) -> str: raise Exception("abstract method") + def translate(self, v: Union[list, Point]): + """Apply a translation""" + return Translate(v=v, child=self) + def move(self, v: Union[list, Point]): - return Translate(v, self) + """Apply a translation""" + return Translate(v=v, child=self) + + def rotate(self, a, v: Union[list, Point]): + """Apply a rotation""" + return Rotate(a=a, v=v, child=self) class Cube(Object): def __init__(self, size: Union[list, Point], center: bool = False): - self.size = Point.c(position) + self.size = Point.c(size) self.center = center def render(self): return f"cube(size=[{self.size.render()}], center={self._center()});" -def Sphere(Object): +class Sphere(Object): def __init__(self, r): self.r = r @@ -148,16 +175,37 @@ def Sphere(Object): return f"sphere(r={self.r});" -def Cylinder(Object): +class Cylinder(Object): def __init__(self, h, r=None, r1=None, r2=None, center: bool = False): self.height = h self.r1 = r if r1 is None else r1 self.r2 = r if r2 is None else r2 + self.center = center # $fa, $fs, $fn def render(self): 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: + """Construct a cylinder between two points""" + p1 = Point.c(p1) + p2 = Point.c(p2) + v = p2.sub(p1) + length = v.length() + assert length != 0 + z = Point([0, 0, 1]) + r = v.cross(z).norm() + rangle = v.angle(z) + if r.length() == 0: + # The cylinder is in the Z direction + if abs(abs(rangle) - 180.) < .1: + p1 = p2 + rangle = 0 + r = z + return cls(h=length, r=radius, center=False).rotate(a=rangle, v=r).move(p1) + # TODO polyhedron(points=[[],], faces[[p,],], convexity=) @@ -177,11 +225,26 @@ class Collection(Object): class Translate(Object): + """Represents a translation transformation applied to an object""" def __init__(self, v: Union[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}}" +class Rotate(Object): + """Represents a rotation transformation applied to an object""" + + def __init__(self, a, v: Union[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}}" + + + diff --git a/test.py b/test.py new file mode 100644 index 0000000..82c4267 --- /dev/null +++ b/test.py @@ -0,0 +1,7 @@ + +from openscad_py import Cube, Cylinder + + +print(Cube([1,1,1]).render()) +print(Cylinder(h=5, r=2).render()) +print(Cylinder.from_ends(2, [0,0,0], [1,0,0]).render())