# Copyright (c) 2018 Manfred Moitzi # License: MIT License from typing import TYPE_CHECKING, Tuple, Sequence, Iterable from .vector import Vector, X_AXIS, Y_AXIS, Z_AXIS if TYPE_CHECKING: from ezdxf.eztypes import GenericLayoutType, Vertex def render_axis(layout: 'GenericLayoutType', start: 'Vertex', points: Sequence['Vertex'], colors: Tuple[int, int, int] = (1, 3, 5)) -> None: for point, color in zip(points, colors): layout.add_line(start, point, dxfattribs={'color': color}) class Matrix33: """ Simple 3x3 Matrix for coordinate transformation. """ __slots__ = ('ux', 'uy', 'uz') def __init__(self, ux: 'Vertex' = (1, 0, 0), uy: 'Vertex' = (0, 1, 0), uz: 'Vertex' = (0, 0, 1)): self.ux = Vector(ux) self.uy = Vector(uy) self.uz = Vector(uz) def transpose(self) -> 'Matrix33': return Matrix33( (self.ux.x, self.uy.x, self.uz.x), (self.ux.y, self.uy.y, self.uz.y), (self.ux.z, self.uy.z, self.uz.z), ) def transform(self, vector: 'Vertex') -> Vector: px, py, pz = Vector(vector) ux = self.ux uy = self.uy uz = self.uz x = px * ux.x + py * uy.x + pz * uz.x y = px * ux.y + py * uy.y + pz * uz.y z = px * ux.z + py * uy.z + pz * uz.z return Vector(x, y, z) class OCS: def __init__(self, extrusion: 'Vertex' = Z_AXIS): Az = Vector(extrusion).normalize() self.transform = not Az.isclose(Z_AXIS) if self.transform: if (abs(Az.x) < 1 / 64.) and (abs(Az.y) < 1 / 64.): Ax = Y_AXIS.cross(Az) else: Ax = Z_AXIS.cross(Az) Ax = Ax.normalize() Ay = Az.cross(Ax).normalize() self.matrix = Matrix33(Ax, Ay, Az) self.transpose = self.matrix.transpose() @property def ux(self) -> Vector: return self.matrix.ux if self.transform else X_AXIS @property def uy(self) -> Vector: return self.matrix.uy if self.transform else Y_AXIS @property def uz(self) -> Vector: return self.matrix.uz if self.transform else Z_AXIS def from_wcs(self, point: 'Vertex') -> 'Vertex': if self.transform: return self.transpose.transform(point) else: return point def points_from_wcs(self, points: Iterable['Vertex']) -> Iterable['Vertex']: for point in points: yield self.from_wcs(point) def to_wcs(self, point: 'Vertex') -> 'Vertex': if self.transform: return self.matrix.transform(point) else: return point def points_to_wcs(self, points: Iterable['Vertex']) -> Iterable['Vertex']: for point in points: yield self.to_wcs(point) def render_axis(self, layout: 'GenericLayoutType', length: float = 1, colors: Tuple[int, int, int] = (1, 3, 5)): render_axis( layout, start=(0, 0, 0), points=( self.to_wcs(X_AXIS * length), self.to_wcs(Y_AXIS * length), self.to_wcs(Z_AXIS * length), ), colors=colors, ) class UCS: def __init__(self, origin: 'Vertex' = (0, 0, 0), ux: 'Vertex' = None, uy: 'Vertex' = None, uz: 'Vertex' = None): self.origin = Vector(origin) if ux is None and uy is None: ux = X_AXIS uy = Y_AXIS uz = Z_AXIS elif ux is None: uy = Vector(uy).normalize() uz = Vector(uz).normalize() ux = Vector(uy).cross(uz).normalize() elif uy is None: ux = Vector(ux).normalize() uz = Vector(uz).normalize() uy = Vector(uz).cross(ux).normalize() elif uz is None: ux = Vector(ux).normalize() uy = Vector(uy).normalize() uz = Vector(ux).cross(uy).normalize() else: # all axis are given ux = Vector(ux).normalize() uy = Vector(uy).normalize() uz = Vector(uz).normalize() self.matrix = Matrix33(ux, uy, uz) self.transpose = self.matrix.transpose() @property def ux(self) -> Vector: return self.matrix.ux @property def uy(self) -> Vector: return self.matrix.uy @property def uz(self) -> Vector: return self.matrix.uz def to_wcs(self, point: 'Vertex') -> Vector: """ Calculate world coordinates for point in UCS coordinates. """ return self.origin + self.matrix.transform(point) def points_to_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]: """ Translate multiple user coordinates into world coordinates (generator). """ for point in points: yield self.to_wcs(point) def to_ocs(self, point: 'Vertex') -> 'Vertex': """ Calculate OCS coordinates for point in UCS coordinates. OCS is defined by the z-axis of the UCS. """ wpoint = self.to_wcs(point) return OCS(self.uz).from_wcs(wpoint) def points_to_ocs(self, points: Iterable['Vertex']) -> Iterable['Vertex']: """ Translate multiple user coordinates into OCS coordinates (generator). OCS is defined by the z-axis of the UCS. """ wcs = self.to_wcs ocs = OCS(self.uz) for point in points: yield ocs.from_wcs(wcs(point)) def to_ocs_angle_deg(self, angle: float) -> float: """ Transform angle in UCS xy-plane to angle in OCS xy-plane. Args: angle: in UCS in degrees Returns: angle in OCS in degrees """ vec = Vector.from_deg_angle(angle) vec = self.to_ocs(vec) - self.origin return vec.angle_deg def to_ocs_angle_rad(self, angle: float) -> float: """ Transform angle in UCS xy-plane to angle in OCS xy-plane. Args: angle: in UCS in radians Returns: angle in OCS in radians """ vec = Vector.from_angle(angle) vec = self.to_ocs(vec) - self.origin return vec.angle def from_wcs(self, point: 'Vertex') -> Vector: """ Calculate UCS coordinates for point in world coordinates. """ return self.transpose.transform(point - self.origin) def points_from_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]: """ Translate multiple world coordinates into user coordinates (generator). """ for point in points: yield self.from_wcs(point) @property def is_cartesian(self) -> bool: return self.uy.cross(self.uz).isclose(self.ux) @staticmethod def from_x_axis_and_point_in_xy(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS': x_axis = Vector(axis) z_axis = x_axis.cross(Vector(point) - origin) return UCS(origin=origin, ux=x_axis, uz=z_axis) @staticmethod def from_x_axis_and_point_in_xz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS': x_axis = Vector(axis) xz_vector = Vector(point) - origin y_axis = xz_vector.cross(x_axis) return UCS(origin=origin, ux=x_axis, uy=y_axis) @staticmethod def from_y_axis_and_point_in_xy(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS': y_axis = Vector(axis) xy_vector = Vector(point) - origin z_axis = xy_vector.cross(y_axis) return UCS(origin=origin, uy=y_axis, uz=z_axis) @staticmethod def from_y_axis_and_point_in_yz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS': y_axis = Vector(axis) yz_vector = Vector(point) - origin x_axis = yz_vector.cross(y_axis) return UCS(origin=origin, ux=x_axis, uy=y_axis) @staticmethod def from_z_axis_and_point_in_xz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS': z_axis = Vector(axis) y_axis = z_axis.cross(Vector(point) - origin) return UCS(origin=origin, uy=y_axis, uz=z_axis) @staticmethod def from_z_axis_and_point_in_yz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS': z_axis = Vector(axis) yz_vector = Vector(point) - origin x_axis = yz_vector.cross(z_axis) return UCS(origin=origin, ux=x_axis, uz=z_axis) def render_axis(self, layout: 'GenericLayoutType', length: float = 1, colors: Tuple[int, int, int] = (1, 3, 5)): render_axis( layout, start=self.origin, points=( self.to_wcs(X_AXIS * length), self.to_wcs(Y_AXIS * length), self.to_wcs(Z_AXIS * length), ), colors=colors, ) class PassTroughUCS(UCS): """ UCS is equal to the WCS and OCS (extrusion = 0, 0, 1) """ def __init__(self): super().__init__() def to_wcs(self, point: 'Vertex') -> Vector: return Vector(point) def points_to_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]: for point in points: yield Vector(point) def to_ocs(self, point: 'Vertex') -> 'Vertex': return Vector(point) def points_to_ocs(self, points: Iterable['Vertex']) -> Iterable['Vertex']: for point in points: yield Vector(point) def to_ocs_angle_deg(self, angle: float) -> float: return angle def to_ocs_angle_rad(self, angle: float) -> float: return angle def from_wcs(self, point: 'Vertex') -> Vector: return Vector(point) def points_from_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]: for point in points: yield Vector(point)