2023-12-10 22:38:11 +00:00
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
from typing import Union as TUnion
|
2024-03-05 00:11:55 +00:00
|
|
|
from typing import List
|
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
|
2023-12-11 01:19:40 +00:00
|
|
|
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"""
|
2023-12-11 01:19:40 +00:00
|
|
|
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
|
|
|
|
2024-03-05 00:11:55 +00:00
|
|
|
def angle(self, p: 'Point', mode: str = "deg") -> float:
|
|
|
|
"""Return the angle between two vectors in degrees or radians"""
|
2023-12-10 23:41:28 +00:00
|
|
|
r = self.dot(p)
|
|
|
|
r = r / self.length() / p.length()
|
|
|
|
r = math.acos(r)
|
2024-03-05 00:11:55 +00:00
|
|
|
if mode == "rad":
|
|
|
|
return r
|
|
|
|
if mode == "deg":
|
|
|
|
return r / math.pi * 180.
|
|
|
|
raise ValueError("Unknown mode")
|
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')
|
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
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")
|
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
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)
|
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
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)
|
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
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
|
|
|
|
2023-12-11 01:19:40 +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:
|
|
|
|
|
2024-03-03 02:46:56 +00:00
|
|
|
def __init__(self, quality: str = 'draft'):
|
|
|
|
self.quality = quality
|
2024-03-03 02:42:07 +00:00
|
|
|
|
|
|
|
def render(self):
|
2024-03-05 00:11:55 +00:00
|
|
|
# See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features#Circle_resolution:_$fa,_$fs,_and_$fn
|
2024-03-03 02:46:56 +00:00
|
|
|
if self.quality == 'draft':
|
|
|
|
return ""
|
|
|
|
if self.quality == 'mid':
|
|
|
|
return "$fa=12;$fs=0.2;"
|
|
|
|
if self.quality == 'best':
|
|
|
|
return "$fa=6;$fs=0.1;"
|
|
|
|
raise ValueError("Unknown quality")
|
2024-03-03 02:42:07 +00:00
|
|
|
|
|
|
|
|
2023-12-10 22:38:11 +00:00
|
|
|
class Cube(Object):
|
2024-03-05 00:11:55 +00:00
|
|
|
"""A 3D primitive, cube.
|
|
|
|
Creates a cube in the first octant. When center is true, the cube is centered on the origin."""
|
|
|
|
# https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/The_OpenSCAD_Language#cube
|
2023-12-10 22:38:11 +00:00
|
|
|
|
2023-12-11 01:19:40 +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):
|
2023-12-11 01:19:40 +00:00
|
|
|
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):
|
2024-03-05 00:11:55 +00:00
|
|
|
"""A 3D primitive, sphere.
|
|
|
|
Creates a sphere at the origin of the coordinate system."""
|
|
|
|
# https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/The_OpenSCAD_Language#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):
|
2024-03-05 00:11:55 +00:00
|
|
|
"""A 3D primitive, cylinder.
|
|
|
|
Creates a cylinder or cone centered about the z axis. When center is true, it is also centered vertically along the z axis."""
|
|
|
|
# https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/The_OpenSCAD_Language#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
|
2023-12-11 01:19:40 +00:00
|
|
|
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-05 00:11:55 +00:00
|
|
|
r = z.cross(v)
|
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
|
2024-03-05 00:11:55 +00:00
|
|
|
else:
|
|
|
|
r = r.norm()
|
2023-12-10 23:41:28 +00:00
|
|
|
return cls(h=length, r=radius, center=False).rotate(a=rangle, v=r).move(p1)
|
2023-12-10 22:38:11 +00:00
|
|
|
|
|
|
|
|
2024-03-05 00:11:55 +00:00
|
|
|
class Polyhedron(Object):
|
|
|
|
"""A 3D primitive, a polyhedron defined by a list of points and faces."""
|
|
|
|
# See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/The_OpenSCAD_Language#polyhedron
|
|
|
|
# Nonplanar faces should be triangulated by opensCAD
|
|
|
|
|
|
|
|
def __init__(self, points: List[TUnion[list, Point]], faces: List[list], convexity: int = 10):
|
|
|
|
self.points = [Point.c(p) for p in points]
|
|
|
|
self.faces = faces
|
|
|
|
self.convexity = convexity
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def tube(cls, points: List[List[TUnion[list, Point]]], convexity: int = 10):
|
|
|
|
"""Construct a tube-like polyhedron from a 2D array of points.
|
|
|
|
Each row of points must be oriented clockwise when looking at the pipe at the start inwards.
|
|
|
|
The rows of points form loops.
|
|
|
|
"""
|
|
|
|
rows = len(points)
|
|
|
|
row_len = len(points[0])
|
|
|
|
point_list = []
|
|
|
|
point_map = {} # { (row_ix,col_ix) -> list_ix, ...
|
|
|
|
for row_ix, row in enumerate(points):
|
|
|
|
for col_ix, point in enumerate(row):
|
|
|
|
point_map[(row_ix, col_ix)] = len(point_list)
|
|
|
|
point_list.append(point)
|
|
|
|
|
|
|
|
faces = []
|
|
|
|
|
|
|
|
# Side faces
|
|
|
|
for row_ix in range(1, rows):
|
|
|
|
for col_ix in range(1, row_len):
|
|
|
|
faces.append([
|
|
|
|
point_map[(row_ix, col_ix-1)],
|
|
|
|
point_map[(row_ix, col_ix)],
|
|
|
|
point_map[(row_ix-1, col_ix)],
|
|
|
|
point_map[(row_ix-1, col_ix-1)]
|
|
|
|
])
|
|
|
|
faces.append([
|
|
|
|
point_map[(row_ix, row_len-1)],
|
|
|
|
point_map[(row_ix, 0)],
|
|
|
|
point_map[(row_ix-1, 0)],
|
|
|
|
point_map[(row_ix-1, row_len-1)]
|
|
|
|
])
|
|
|
|
|
|
|
|
# Starting cap
|
|
|
|
faces.append([point_map[(0,x)] for x in range(row_len)])
|
|
|
|
|
|
|
|
# Ending cap
|
|
|
|
faces.append([point_map[(rows-1,row_len-1-x)] for x in range(row_len)])
|
|
|
|
|
|
|
|
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]
|
|
|
|
return f"polyhedron(points=[{','.join([p.render() for p in self.points])}], faces=[{','.join(faces_list)}], convexity={self.convexity});"
|
|
|
|
|
|
|
|
|
|
|
|
class PathTube(Object):
|
|
|
|
"""Creates a tube-like or toroid polyhedron from a path (list of points)."""
|
|
|
|
|
|
|
|
def __init__(self, points: List[TUnion[list, Point]], radius: float, fn: int, convexity: int = 10):
|
|
|
|
self.points = [Point.c(p) for p in points]
|
|
|
|
self.radius = radius
|
|
|
|
self.fn = fn # number of sides
|
|
|
|
self.convexity = convexity
|
|
|
|
|
|
|
|
def process(self, debug: bool = False) -> Polyhedron:
|
|
|
|
points_rows = []
|
|
|
|
|
|
|
|
for ix, point in enumerate(self.points):
|
|
|
|
if debug: print(f"//LOOP {ix}: {point.render()}")
|
|
|
|
|
|
|
|
if ix == 0:
|
|
|
|
# Start of the path
|
|
|
|
v = self.points[1].sub(point) # vector toward the first point
|
|
|
|
z_point = Point([0,0,1])
|
|
|
|
seam = v.cross(z_point) # Track a seam along the pipe using this vector pointing from the middle line
|
|
|
|
if seam.length() == 0: # v is in the z direction
|
|
|
|
seam = Point([1,0,0])
|
|
|
|
seam = seam.norm()
|
|
|
|
seam2 = v.cross(seam).norm()
|
|
|
|
if debug: print(f"//Start. v={v.render()} seam={seam.render()} seam2={seam2.render()}")
|
|
|
|
points = []
|
|
|
|
for i in range(self.fn):
|
|
|
|
a = math.pi*2*i/self.fn
|
|
|
|
points.append((seam*math.cos(a) + seam2*math.sin(a))*self.radius + point)
|
|
|
|
points_rows.append(points)
|
|
|
|
if debug: print(f"// Row: {', '.join([p.render() for p in points])}")
|
|
|
|
|
|
|
|
elif ix == len(self.points) - 1:
|
|
|
|
# End of the path
|
|
|
|
v = point.sub(self.points[-2])
|
|
|
|
seam2 = v.cross(seam).norm()
|
|
|
|
if debug: print(f"//End. v={v.render()} seam={seam.render()} seam2={seam2.render()}")
|
|
|
|
points = []
|
|
|
|
for i in range(self.fn):
|
|
|
|
a = math.pi*2*i/self.fn
|
|
|
|
points.append((seam*math.cos(a) + seam2*math.sin(a))*self.radius + point)
|
|
|
|
points_rows.append(points)
|
|
|
|
if debug: print(f"// Row: {', '.join([p.render() for p in points])}")
|
|
|
|
|
|
|
|
else:
|
|
|
|
# Middle of the path
|
|
|
|
# (p[-1]) -va-> (p[0]) -vb-> (p[1])
|
|
|
|
va = point.sub(self.points[ix-1]).norm() # vector incoming to this elbow
|
|
|
|
vb = self.points[ix+1].sub(point).norm() # vector going out from this elbow
|
|
|
|
if debug: print(f"//Middle. va={va.render()} vb={vb.render()}")
|
|
|
|
# Get the vector perpendicular to va that points to the inside of the cylinder around va according
|
|
|
|
# to the elbow at p[0]. This is the component of vb in a basis defined by va.
|
|
|
|
vdot = va.dot(vb)
|
|
|
|
vb_proj = va.scale(vdot) # The projection of vb onto va
|
|
|
|
vb_perp = vb.sub(vb_proj) # This is perpendicular to va
|
|
|
|
if debug: print(f"// vb_proj={vb_proj.render()} vb_perp={vb_perp.render()}")
|
|
|
|
va_inner = vb_perp.norm()
|
|
|
|
|
|
|
|
va_proj = vb.scale(vdot)
|
|
|
|
va_perp = va.sub(va_proj)
|
|
|
|
if debug: print(f"// va_proj={va_proj.render()} va_perp={va_perp.render()}")
|
|
|
|
vb_inner = va_perp.scale(-1).norm() # Here we want to project -va onto vb
|
|
|
|
if debug: print(f"// va_inner={va_inner.render()} vb_inner={vb_inner.render()}")
|
|
|
|
|
|
|
|
# The new seam on vb (seam_b) has the same angle to vb_inner as it had on va to va_inner
|
|
|
|
seam_angle = seam.angle(va_inner, mode="rad")
|
|
|
|
# need to figure out the sign of the angle
|
|
|
|
if seam_angle != 0:
|
|
|
|
if va_inner.cross(seam).dot(va) < 0:
|
|
|
|
seam_angle = -seam_angle
|
|
|
|
vb_inner2 = vb.cross(vb_inner).norm()
|
|
|
|
seam_b = vb_inner*math.cos(seam_angle) + vb_inner2*math.sin(seam_angle)
|
|
|
|
if debug: print(f"// seam={seam.render()} seam_b={seam_b.render()}")
|
|
|
|
|
|
|
|
vangle = va.scale(-1).angle(vb, mode="rad")
|
|
|
|
long_inner = (vb-va).norm().scale(1/math.sin(vangle/2))
|
|
|
|
# long_inner is the long axis of the elliptic intersection between the cylinders around va and vb
|
|
|
|
short = va.cross(long_inner).norm() # the short axis of the ellipse
|
|
|
|
if debug: print(f"// long_inner={long_inner.render()} short={short.render()} vangle={vangle/math.pi*180}(deg) seam_angle={seam_angle/math.pi*180}(deg)")
|
|
|
|
points = []
|
|
|
|
for i in range(self.fn):
|
|
|
|
# We draw the ellipse according to long_inner and short, but use seam_angle to get the right points
|
|
|
|
a = math.pi*2*i/self.fn + seam_angle
|
|
|
|
points.append((long_inner*math.cos(a) + short*math.sin(a))*self.radius + point)
|
|
|
|
points_rows.append(points)
|
|
|
|
if debug: print(f"// Row: {', '.join([p.render() for p in points])}")
|
|
|
|
|
|
|
|
seam = seam_b
|
|
|
|
|
|
|
|
return Polyhedron.tube(points=points_rows, convexity=self.convexity)
|
|
|
|
|
|
|
|
def render(self) -> str:
|
|
|
|
return self.process().render()
|
|
|
|
|
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
class Circle(Object):
|
2024-03-05 00:11:55 +00:00
|
|
|
"""A 2D primitive, circle.
|
|
|
|
Creates a circle (or regular polygon) at the origin."""
|
|
|
|
# https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/The_OpenSCAD_Language#circle
|
2023-12-11 01:19:40 +00:00
|
|
|
|
2024-03-05 00:11:55 +00:00
|
|
|
def __init__(self, r: float, fn: TUnion[int, None] = None):
|
2023-12-11 01:19:40 +00:00
|
|
|
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});"
|
|
|
|
|
2024-03-05 00:11:55 +00:00
|
|
|
|
2023-12-10 23:02:49 +00:00
|
|
|
# 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
|
2023-12-11 01:19:40 +00:00
|
|
|
|
|
|
|
@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
|
|
|
|
2023-12-11 01:19:40 +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
|
|
|
|
2023-12-11 01:19:40 +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
|
|
|
|
2023-12-11 01:19:40 +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"""
|
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
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
|
|
|
|
|
2023-12-11 01:19:40 +00:00
|
|
|
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()} }}"
|
|
|
|
|