csirmaz.openscad-py/openscad_py/point.py

191 lines
6 KiB
Python
Raw Normal View History

2024-11-30 22:11:59 +00:00
from typing import Union as TUnion
from typing import List
import math
import numpy as np
EPSILON = 1e-7
NP_TYPE = np.float_
class Point:
2024-12-02 19:39:18 +00:00
"""Represents a point or vector in arbitrary number of dimensions"""
2024-11-30 22:11:59 +00:00
2024-12-02 19:39:18 +00:00
def __init__(self, coords: List[float]):
2024-11-30 22:11:59 +00:00
self.c = np.array(coords, dtype=NP_TYPE)
@classmethod
2024-12-02 19:39:18 +00:00
def c(cls, coords: TUnion[list[float], 'Point']) -> 'Point':
"""Ensure `coords` is an instance of Point (idempotent)"""
2024-11-30 22:11:59 +00:00
if isinstance(coords, Point):
return coords
return Point(coords)
def render(self) -> str:
2024-12-02 19:39:18 +00:00
"""Render the point / vector into OpenSCAD code"""
2024-11-30 22:11:59 +00:00
return "[" + (",".join([str(c) for c in self.c])) + "]"
def render_stl(self) -> str:
2024-12-02 19:39:18 +00:00
"""Render the point / vector into STL"""
2024-11-30 22:11:59 +00:00
return " ".join([str(c) for c in self.c])
def scale(self, x: float) -> 'Point':
2024-12-02 19:39:18 +00:00
"""Scale the current point / vector by the scalar `x`"""
2024-11-30 22:11:59 +00:00
return self.__class__(self.c * x)
def add(self, p: 'Point') -> 'Point':
2024-12-02 19:39:18 +00:00
"""Add another point / vector `p` to the current one"""
2024-11-30 22:11:59 +00:00
assert isinstance(p, Point)
assert self.dim() == p.dim()
return self.__class__(self.c + p.c)
def sub(self, p: 'Point') -> 'Point':
2024-12-02 19:39:18 +00:00
"""Subtract another point / vector `p` from the current one"""
2024-11-30 22:11:59 +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())
def norm(self) -> 'Point':
2024-12-02 19:39:18 +00:00
"""Return a normalized version of the vector (scaled to length 1)"""
2024-11-30 22:11:59 +00:00
l = self.length()
if l == 0:
2024-12-02 19:39:18 +00:00
raise Exception("Attempted to normalise 0 vector")
2024-11-30 22:11:59 +00:00
return self.__class__(self.c / self.length())
def dot(self, p: 'Point') -> float:
2024-12-02 19:39:18 +00:00
"""Return the dot product of the current vector and `p`"""
2024-11-30 22:11:59 +00:00
return np.dot(self.c, p.c)
def cross(self, p: 'Point') -> 'Point':
2024-12-02 19:39:18 +00:00
"""Return the cross product of the current vector and `p`"""
2024-11-30 22:11:59 +00:00
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:
2024-12-02 19:39:18 +00:00
"""Return whether the current point / vector and `p` are equal"""
2024-11-30 22:11:59 +00:00
return (self.c == p.c).all()
def lt(self, p: 'Point') -> bool:
2024-12-02 19:39:18 +00:00
"""Return whether the current vector is smaller than `p` in each dimension"""
2024-11-30 22:11:59 +00:00
return (self.c < p.c).all()
def le(self, p: 'Point') -> bool:
2024-12-02 19:39:18 +00:00
"""Return whether the current vector is smaller or equal to `p` in each dimension"""
2024-11-30 22:11:59 +00:00
return (self.c <= p.c).all()
def gt(self, p: 'Point') -> bool:
2024-12-02 19:39:18 +00:00
"""Return whether the current vector is greater than `p` in each dimension"""
2024-11-30 22:11:59 +00:00
return (self.c > p.c).all()
def ge(self, p: 'Point') -> bool:
2024-12-02 19:39:18 +00:00
"""Return whether the current vector is greater or equal to `p` in each dimension"""
2024-11-30 22:11:59 +00:00
return (self.c >= p.c).all()
def allclose(self, p: 'Point') -> bool:
2024-12-02 19:39:18 +00:00
"""Return whether the current point / vector and `p` are close to each other"""
2024-11-30 22:11:59 +00:00
return self.c.shape == p.c.shape and np.allclose(self.c, p.c)
def angle(self, p: 'Point', mode: str = "deg") -> float:
2024-12-02 19:39:18 +00:00
"""Return the angle between two vectors in degrees or radians
Arguments:
- p: a Point object
- mode: "deg" | "rad"
"""
2024-11-30 22:11:59 +00:00
r = self.dot(p)
r = r / self.length() / p.length()
r = math.acos(r)
if mode == "rad":
return r
if mode == "deg":
return r / math.pi * 180.
raise ValueError("Unknown mode")
def z_slope(self, mode: str = "deg") -> float:
2024-12-02 19:39:18 +00:00
"""Return the slope of a vector in degrees or radians
Arguments:
- mode: "deg" | "rad"
"""
2024-11-30 22:11:59 +00:00
r = self.c[2] / self.length()
r = math.asin(r)
if mode == "rad":
return r
if mode == "deg":
return r / math.pi * 180.
raise ValueError("Unknown mode")
def rotate(self, coords, angle: float) -> 'Point':
2024-12-02 19:39:18 +00:00
"""Rotate the current vector
Arguments:
- coords: A list of 2 coordinate indices to rotate
- angle: the angle to rotate by, in degrees
"""
2024-11-30 22:11:59 +00:00
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):
2024-12-02 19:39:18 +00:00
"""Use `p1 + p2` to add two vectors"""
2024-11-30 22:11:59 +00:00
return self.add(other)
def __radd__(self, other):
2024-12-02 19:39:18 +00:00
"""Use `p1 + p2` to add two vectors"""
2024-11-30 22:11:59 +00:00
assert isinstance(other, Point)
return other.add(self)
def __sub__(self, other):
2024-12-02 19:39:18 +00:00
"""Use `p1 - p2` to subtract two vectors"""
2024-11-30 22:11:59 +00:00
return self.sub(other)
def __rsub__(self, other):
2024-12-02 19:39:18 +00:00
"""Use `p1 - p2` to subtract two vectors"""
2024-11-30 22:11:59 +00:00
assert isinstance(other, Point)
return other.sub(self)
def __mul__(self, other):
2024-12-02 19:39:18 +00:00
"""Use `p * x` to scale a vector"""
2024-11-30 22:11:59 +00:00
return self.scale(other)
def __rmul__(self, other):
2024-12-02 19:39:18 +00:00
"""Use `x * p` to scale a vector"""
2024-11-30 22:11:59 +00:00
return self.scale(other)
def __neg__(self):
2024-12-02 19:39:18 +00:00
"""Use `-p` to negate a vector"""
2024-11-30 22:11:59 +00:00
return self.scale(-1.)
2024-12-02 19:39:18 +00:00
__pdoc__ = {
'Point.__add__': True,
'Point.__sub__': True,
'Point.__mul__': True,
'Point.__rmul__': True,
'Point.__neg__': True,
}