#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Geometry objects.
:Date: 2019-12-21
.. module:: geometry
:platform: *nix, Windows
:synopsis: Geometry objects.
.. moduleauthor:: Daniel Weschke <daniel.weschke@directbox.de>
.. rubric:: Affine transforms
Functions in augmented space, in homogenous coordinates.
Points are augment to 4 dimensions, by adding a dummy coordinate.
For points the dummy coordinate is always normalized to 1.
With homogenous coordinates translation of points is repesentable
as a linear transformation.
"""
import math
import copy
from .data import seq
from .mathematics import vector, matrix
from .function import circle, ellipse, b_spline_curve_with_knots
[docs]class Properties():
circle_sectors = 12 # 12 = 30°
[docs]class Direction(vector):
"""Direction in local coordinate system"""
def __init__(self, x=1, y=0, z=0):
super().__init__([x, y, z, 0])
[docs] @classmethod
def cross(cls, a, b):
return cls(*vector.cross(a[:3], b[:3]))
[docs]class Point(vector):
"""Point in local coordinate system"""
def __init__(self, x=0, y=0, z=0):
super().__init__([x, y, z, 1])
# TODO
[docs] def projection(self):
"""Orthographic projection to the xy-plane
"""
# P = matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
#return (P*self)[:2]
return self[:2]
[docs]class CS(matrix):
"""Coordinate system
"""
def __init__(self, x=Direction(1, 0, 0), y=Direction(0, 1, 0),
z=Direction(0, 0, 1)):
super().__init__([x, y, z, vector([0, 0, 0, 1])])
[docs] @classmethod
def init_xyz(cls, x=[1, 0, 0], y=[0, 1, 0], z=[0, 0, 1]):
return cls(Direction(*x), Direction(*y), Direction(*z))
[docs] @classmethod
def x90(cls):
return cls(Direction(1, 0, 0), Direction(0, 0, -1),
Direction(0, 1, 0))
[docs] @classmethod
def xm90(cls):
return cls(Direction(1, 0, 0), Direction(0, 0, 1),
Direction(0, -1, 0))
[docs] @classmethod
def y90(cls):
return cls(Direction(0, 0, 1), Direction(0, 1, 0),
Direction(-1, 0, 0))
[docs] @classmethod
def ym90(cls):
return cls(Direction(0, 0, -1), Direction(0, 1, 0),
Direction(1, 0, 0))
[docs] def get_coordinates(self):
"""Get coordinates in 3d space"""
return self[:3,:3]
# TODO: Polyline(list) or Polyline(matrix) ?
# list of Points
[docs]class Polyline:
"""Open and closed wireframe object in local coordinate system
This class create its own points (copy).
"""
def __init__(self, *points, closed=False):
self._points = [copy.copy(i) for i in points]
self.closed = closed
[docs] def __str__(self):
return '[' + ', '.join([str(point) for point in self._points]) + ']'
[docs] def __repr__(self):
return self.__class__.__name__ + \
'(' + ', '.join([str(point) for point in self._points]) + ')'
[docs] def __iter__(self):
"""Returns the Iterator object"""
return iter(self.points())
[docs] def points(self):
"""Get coordinates in 3d space"""
result = [i for i in self._points]
return result if not self.closed else result + [result[0]]
[docs] def xy(self):
"""Get coordinates in 3d space"""
return list(zip(*[i[:2] for i in self.points()]))
[docs] def xyz(self):
"""Get coordinates in 3d space"""
return list(zip(*[i[:3] for i in self.points()]))
[docs] def rotate_x(self, theta):
self._points = [point.rotate_x(theta) for point in self._points]
return self
[docs] def rotate_y(self, theta):
self._points = [point.rotate_y(theta) for point in self._points]
return self
[docs] def rotate_z(self, theta):
self._points = [point.rotate_z(theta) for point in self._points]
return self
[docs] def translate(self, tx, ty, tz):
self._points = [point.translate(tx, ty, tz) for
point in self._points]
return self
[docs] def scale(self, sx, sy=None, sz=None):
# if not sy is not suitable because 0 is also false
if sy is None:
sy = sx
sz = sx
self._points = [point.scale(sx, sy, sz) for
point in self._points]
return self
[docs] def ch_cs(self, cs):
self._points = [point.ch_cs(cs) for point in self._points]
return self
[docs]class Line(Polyline):
"""Line, an open wireframe object in local coordinate system"""
def __init__(self, point1=Point(-1, 0, 0), point2=Point(1, 0, 0)):
super().__init__(point1, point2)
# TODO: combining wit non Arc version?
[docs]class ArcCircle(Polyline):
"""Arc of a circle, an open wireframe object in local coordinate
system
"""
def __init__(self, radius=1, ang1=0, ang2=math.pi/2, n=None):
x, y, interval = circle(radius)
if not n:
n = Properties.circle_sectors
delta = interval[1]/n
points = []
points.append(Point(x(ang1), y(ang1), 0))
if ang1 > ang2 :
ang1 = ang1 - 2*math.pi
a, b = int(ang1 // delta) + 1, int(ang2 // delta) + 1
for i in range(a, b):
points.append(Point(x(i*delta), y(i*delta), 0))
points.append(Point(x(ang2), y(ang2), 0))
super().__init__(*points)
# TODO: combining wit non Arc version?
[docs]class ArcEllipse(Polyline):
"""Arc of an ellipse, an open wireframe object in local
coordinate system
"""
def __init__(self, a=1, b=1, ang1=0, ang2=math.pi/2, n=None):
x, y, interval = ellipse(a, b)
if not n:
n = Properties.circle_sectors
delta = interval[1]/n
points = []
points.append(Point(x(ang1), y(ang1), 0))
if ang1 > ang2 :
ang1 = ang1 - 2*math.pi
a, b = int(ang1 // delta) + 1, int(ang2 // delta) + 1
for i in range(a, b):
points.append(Point(x(i*delta), y(i*delta), 0))
points.append(Point(x(ang2), y(ang2), 0))
super().__init__(*points)
# redefining the .function.sample_half_open(f, a, b, n=50, endpoint_epsilon=1e-7)
# to create a list of Points
[docs]def sample_half_open(f, a, b, n=50, endpoint_epsilon=1e-7):
# hack to sample close to the endpoint
x = seq(a, b, (b-a)/n) + [b-endpoint_epsilon]
# Sample the function
return [Point(*f(xi)) for xi in x]
# TODO: naming? combining wit non Arc version?
[docs]class ArcBSplineCurveWithKnots(Polyline):
"""B-spline curve with knots, an open wireframe object in local
coordinate system"""
def __init__(self, degree, control_points, knot_multiplicities,
knots, start, end, n=5):
knots = [knots[i] for i in range(len(knots)) for
j in range(knot_multiplicities[i])]
#u = seq(knots[0], knots[-1], (knots[-1]-knots[0])/n) + [knots[-1]-1e-7]
C = b_spline_curve_with_knots(degree, control_points, knots)
#points = [Point(*C(ui)) for ui in u]
points = sample_half_open(C, start, end, n=n, endpoint_epsilon=1e-7)
super().__init__(*points)
[docs]class B_spline_curve_with_knots(Polyline):
"""B-spline curve with knots, an open wireframe object in local
coordinate system"""
def __init__(self, degree, control_points, knot_multiplicities,
knots, n=5):
knots = [knots[i] for i in range(len(knots)) for
j in range(knot_multiplicities[i])]
#u = seq(knots[0], knots[-1], (knots[-1]-knots[0])/n) + [knots[-1]-1e-7]
C = b_spline_curve_with_knots(degree, control_points, knots)
#points = [Point(*C(ui)) for ui in u]
points = sample_half_open(C, knots[0], knots[-1], n=n, endpoint_epsilon=1e-7)
super().__init__(*points)
[docs]class Polygon(Polyline):
"""Polygon as closed wireframe object in local coordinate system"""
def __init__(self, *points):
super().__init__(*points, closed=True)
[docs]class Circle(Polygon):
"""Circle, a closed wireframe object in local coordinate system"""
def __init__(self, radius=1, n=None):
x, y, interval = circle(radius)
if not n:
n = Properties.circle_sectors
delta = interval[1]/n
points = [Point(x(i*delta), y(i*delta), 0) for i in range(n)]
super().__init__(*points)
[docs]class Ellipse(Polygon):
"""Circle, a closed wireframe object in local coordinate system"""
def __init__(self, a=1, b=1, n=None):
x, y, interval = ellipse(a, b)
if not n:
n = Properties.circle_sectors
delta = interval[1]/n
points = [Point(x(i*delta), y(i*delta), 0) for i in range(n)]
super().__init__(*points)
[docs]class Solid:
"""Solid object in local coordinate system
This class lists Wireframe objects.
The Wireframe class create its own points (copy).
"""
def __init__(self, *wireframes):
self._wireframes = wireframes
[docs] def wireframes(self):
return self._wireframes
[docs] def translate(self, tx, ty, tz):
self._wireframes = [wireframe.translate(tx, ty, tz) for
wireframe in self._wireframes]
return self
[docs] def scale(self, sx, sy=None, sz=None):
# if not sy is not suitable because 0 is also false
if sy is None:
sy = sx
sz = sx
self._wireframes = [wireframe.scale(sx, sy, sz) for
wireframe in self._wireframes]
return self
[docs] def ch_cs(self, cs):
self._wireframes = [wireframe.ch_cs(cs) for
wireframe in self._wireframes]
return self
[docs]class Hexahedron(Solid):
"""Line a open wireframe object in local coordinate system"""
def __init__(self,
point1=Point(-1, -1, -1), point2=Point(1, -1, -1),
point3=Point(1, 1, -1), point4=Point(-1, 1, -1),
point5=Point(-1, -1, 1), point6=Point(1, -1, 1),
point7=Point(1, 1, 1), point8=Point(-1, 1, 1)):
super().__init__(
Polygon(point1, point2, point3, point4),
Polygon(point5, point6, point7, point8),
Polygon(point1, point2, point6, point5),
Polygon(point3, point4, point8, point7)
)
[docs]class World:
"""World-space with world-space coordinates
"""
def __init__(self):
self._cs = CS() # Camera
self._objects = []
self._store_init()
[docs] def __iter__(self):
"""Returns the Iterator object"""
return iter(self.objects())
[docs] def __str__(self):
result = 'World(\n'
for i in self.objects():
result += ' ' + repr(i) + ',\n'
result += ')'
return result
def _store_init(self):
"""Initialize or reset calculated values, because a new object
was added.
"""
self._bb = None
self._sd = None
[docs] def cs(self, cs=None):
if cs:
self._cs = cs
return self._cs
[docs] def ch_cs(self, cs):
self._cs = self._cs * cs
return self
[docs] def rotate_x(self, theta):
self._cs.rotate_x(theta)
return self
[docs] def rotate_y(self, theta):
self._cs.rotate_y(theta)
return self
[docs] def rotate_z(self, theta):
self._cs.rotate_z(theta)
return self
[docs] def translate(self, tx, ty, tz):
self._cs.translate(tx, ty, tz)
return self
[docs] def scale(self, sx, sy=None, sz=None):
self._cs.scale(sx, sy, sz)
return self
[docs] def wireframes(self):
result = []
for i in self.objects():
if isinstance(i, Polyline):
result.append(i.points())
elif isinstance(i, Solid):
[result.append(j.points()) for j in i.wireframes()]
return result
[docs] def wireframes_xy(self):
result = []
for i in self.objects():
if isinstance(i, Polyline):
result.append(i.xy())
elif isinstance(i, Solid):
[result.append(j.xy()) for j in i.wireframes()]
return result
[docs] def wireframes_xyz(self):
result = []
for i in self.objects():
if isinstance(i, Polyline):
result.append(i.xyz())
elif isinstance(i, Solid):
[result.append(j.xyz()) for j in i.wireframes()]
return result
[docs] def objects(self):
return [copy.deepcopy(i).ch_cs(self._cs) for i in self._objects]
[docs] def add(self, *objects):
self._store_init() # calculated values are not correct anymore
# [] + [] not possible bc objects are vectors
[self._objects.append(i) for i in objects]
return self
[docs] def bounding_box(self):
if self._bb is not None:
return self._bb
xmin = math.inf
ymin = math.inf
zmin = math.inf
xmax = -math.inf
ymax = -math.inf
zmax = -math.inf
for i in self.wireframes_xyz():
xi, yi, zi = map(min, i)
xs, ys, zs = map(max, i)
xmin = xi if xi < xmin else xmin
ymin = yi if yi < ymin else ymin
zmin = zi if zi < zmin else zmin
xmax = xs if xs > xmax else xmax
ymax = ys if ys > ymax else ymax
zmax = zs if zs > zmax else zmax
self._bb = xmin, xmax, ymin, ymax, zmin, zmax
return self._bb
[docs] def space_diagonal(self):
if self._sd is not None:
return self._sd
bb = self.bounding_box()
a, b, c = bb[1]-bb[0], bb[3]-bb[2], bb[5]-bb[4]
return math.sqrt(a**2+b**2+c**2)
[docs] def center(self):
bb = self.bounding_box()
self.ch_cs([[1, 0, 0, -(bb[0]+bb[1])/2],
[0, 1, 0, -(bb[2]+bb[3])/2],
[0, 0, 1, -(bb[4]+bb[5])/2],
[0, 0, 0, 1]])
return self