csirmaz.openscad-py/openscad_py.py

391 lines
11 KiB
Python
Raw Normal View History

2023-12-10 22:38:11 +00:00
from typing import Union as TUnion
2023-12-10 23:41:28 +00:00
import math
2023-12-10 23:02:49 +00:00
import numpy as np
EPSILON = 1e-7
NP_TYPE = np.float_
2023-12-10 22:38:11 +00:00
class Point:
2023-12-10 23:41:28 +00:00
"""Represents a point of vector in arbitrary dimensions"""
2023-12-10 22:38:11 +00:00
def __init__(self, coords):
2023-12-10 23:02:49 +00:00
self.c = np.array(coords, dtype=NP_TYPE)
2023-12-10 22:38:11 +00:00
@classmethod
def c(cls, coords: TUnion[list, 'Point']) -> 'Point':
2023-12-10 22:38:11 +00:00
"""Ensure coords is an instance of Point"""
if isinstance(coords, Point):
return coords
return Point(coords)
def render(self) -> str:
2023-12-10 23:41:28 +00:00
"""Render the point into a SCAD script"""
return "[" + (",".join([str(c) for c in self.c])) + "]"
2023-12-10 23:02:49 +00:00
2023-12-10 23:41:28 +00:00
def scale(self, x: float) -> 'Point':
2023-12-10 23:02:49 +00:00
"""Scale the current vector/point by a scalar"""
return self.__class__(self.c * x)
2023-12-10 23:41:28 +00:00
def add(self, p: 'Point') -> 'Point':
2023-12-10 23:02:49 +00:00
assert isinstance(p, Point)
assert self.dim() == p.dim()
return self.__class__(self.c + p.c)
2023-12-10 23:41:28 +00:00
def sub(self, p: 'Point') -> 'Point':
2023-12-10 23:02:49 +00:00
assert isinstance(p, Point)
assert self.dim() == p.dim()
return self.__class__(self.c - p.c)
def dim(self) -> int:
"""Return the number of dimensions"""
return self.c.shape[0]
def is_zero(self) -> bool:
"""Return whether all coordinates are very close to 0"""
return np.all(np.abs(self.c) < EPSILON)
def length(self) -> float:
"""Return the length of the vector"""
return np.sqrt(np.square(self.c).sum())
2023-12-10 23:41:28 +00:00
def norm(self) -> 'Point':
2023-12-10 23:02:49 +00:00
l = self.length()
if l == 0:
raise Exception("normalising 0 vector")
return self.__class__(self.c / self.length())
2023-12-10 23:41:28 +00:00
def dot(self, p: 'Point') -> float:
"""Return the dot product"""
2023-12-10 23:02:49 +00:00
return np.dot(self.c, p.c)
2023-12-10 23:41:28 +00:00
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:
2023-12-10 23:02:49 +00:00
return (self.c == p.c).all()
2023-12-10 23:41:28 +00:00
def lt(self, p: 'Point') -> bool:
2023-12-10 23:02:49 +00:00
return (self.c < p.c).all()
2023-12-10 23:41:28 +00:00
def le(self, p: 'Point') -> bool:
2023-12-10 23:02:49 +00:00
return (self.c <= p.c).all()
2023-12-10 23:41:28 +00:00
def gt(self, p: 'Point') -> bool:
2023-12-10 23:02:49 +00:00
return (self.c > p.c).all()
2023-12-10 23:41:28 +00:00
def ge(self, p: 'Point') -> bool:
2023-12-10 23:02:49 +00:00
return (self.c >= p.c).all()
2023-12-10 23:41:28 +00:00
def allclose(self, p: 'Point') -> bool:
2023-12-10 23:02:49 +00:00
return self.c.shape == p.c.shape and np.allclose(self.c, p.c)
2023-12-10 23:41:28 +00:00
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.
2023-12-10 23:02:49 +00:00
2023-12-10 23:41:28 +00:00
def rotate(self, coords, angle: float) -> 'Point':
2023-12-10 23:02:49 +00:00
"""Rotate. coords is a list of 2 coordinate indices that we rotate"""
assert len(coords) == 2
ca, cb = coords
s = np.sin(angle / 180. * np.pi)
c = np.cos(angle / 180. * np.pi)
r = self.clone().reset_cache()
r.c[ca] = c * self.c[ca] + s * self.c[cb]
r.c[cb] = -s * self.c[ca] + c * self.c[cb]
return r
# Operator overloading
def __add__(self, other):
return self.add(other)
def __radd__(self, other):
assert isinstance(other, Point)
return other.add(self)
2023-12-10 22:38:11 +00:00
2023-12-10 23:02:49 +00:00
def __sub__(self, other):
return self.sub(other)
def __rsub__(self, other):
assert isinstance(other, Point)
return other.sub(self)
def __mul__(self, other):
return self.scale(other)
def __rmul__(self, other):
return self.scale(other)
2023-12-10 22:38:11 +00:00
class Object:
"""Abstract class for an SCAD object"""
def _center(self) -> str:
return ('true' if self.center else 'false')
def _add(self, obj):
"""Add an object, forming a collection"""
2023-12-10 23:41:28 +00:00
return Collection([self, obj])
2023-12-10 22:38:11 +00:00
def render(self) -> str:
raise Exception("abstract method")
def translate(self, v: TUnion[list, Point]) -> 'Object':
2023-12-10 23:41:28 +00:00
"""Apply a translation"""
return Translate(v=v, child=self)
def move(self, v: TUnion[list, Point]) -> 'Object':
2023-12-10 23:41:28 +00:00
"""Apply a translation"""
return Translate(v=v, child=self)
def rotate(self, a, v: TUnion[list, Point]) -> 'Object':
2023-12-10 23:41:28 +00:00
"""Apply a rotation"""
return Rotate(a=a, v=v, child=self)
2023-12-10 22:38:11 +00:00
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))
2023-12-10 22:38:11 +00:00
2024-03-03 02:42:07 +00:00
class Header:
def __init__(self, draft: bool = True):
self.draft = draft
def render(self):
return "" if self.draft else "$fa=6;$fs=0.1;"
2023-12-10 22:38:11 +00:00
class Cube(Object):
"""A 3D primitive, cube"""
2023-12-10 22:38:11 +00:00
def __init__(self, size: TUnion[list, Point], center: bool = False):
2023-12-10 23:41:28 +00:00
self.size = Point.c(size)
2023-12-10 22:38:11 +00:00
self.center = center
def render(self):
return f"cube(size={self.size.render()}, center={self._center()});"
2023-12-10 22:38:11 +00:00
2023-12-10 23:41:28 +00:00
class Sphere(Object):
"""A 3D primitive, sphere"""
2023-12-10 22:38:11 +00:00
def __init__(self, r):
self.r = r
# $fa, $fs, $fn
def render(self):
return f"sphere(r={self.r});"
2023-12-10 23:41:28 +00:00
class Cylinder(Object):
"""A 3D primitive, cylinder"""
2023-12-10 22:38:11 +00:00
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
2023-12-10 23:41:28 +00:00
self.center = center
2023-12-10 22:38:11 +00:00
# $fa, $fs, $fn
def render(self):
return f"cylinder(h={self.height}, r1={self.r1}, r2={self.r2}, center={self._center()});"
2023-12-10 23:41:28 +00:00
@classmethod
def from_ends(cls, radius: float, p1: TUnion[list, Point], p2: TUnion[list, Point]) -> Object:
2023-12-10 23:41:28 +00:00
"""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])
2024-03-03 02:07:13 +00:00
r = z.cross(v).norm()
2023-12-10 23:41:28 +00:00
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)
2023-12-10 22:38:11 +00:00
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});"
2023-12-10 23:02:49 +00:00
# TODO polyhedron(points=[[],], faces[[p,],], convexity=)
# TODO https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types
2023-12-10 22:38:11 +00:00
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)
2023-12-10 22:38:11 +00:00
def _add(self, obj):
return self.__class__(self.collection + [obj])
def render(self) -> str:
2023-12-10 22:38:11 +00:00
return "\n".join([o.render() for o in self.collection])
class Translate(Object):
2023-12-10 23:41:28 +00:00
"""Represents a translation transformation applied to an object"""
2023-12-10 22:38:11 +00:00
def __init__(self, v: TUnion[list, Point], child: Object):
2023-12-10 22:38:11 +00:00
self.v = Point.c(v)
2023-12-10 23:41:28 +00:00
self.child = child
2023-12-10 22:38:11 +00:00
def render(self) -> str:
return f"translate(v={self.v.render()}){{\n{self.child.render()}\n}}"
2023-12-10 22:38:11 +00:00
2023-12-10 23:41:28 +00:00
class Rotate(Object):
"""Represents a rotation transformation applied to an object"""
def __init__(self, a, v: TUnion[list, Point], child: Object):
2023-12-10 23:41:28 +00:00
self.a = a
self.v = Point.c(v)
self.child = child
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()} }}"