1# Copyright (c) 2020, Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, Tuple
4import math
5from ezdxf.math import Vec3, X_AXIS, Y_AXIS, Vec2, Matrix44, sign, OCS
6
7if TYPE_CHECKING:
8    from ezdxf.eztypes import DXFGraphic, Vertex
9
10__all__ = [
11    "NonUniformScalingError", "InsertTransformationError",
12    "transform_extrusion", "transform_thickness_and_extrusion_without_ocs"
13]
14
15
16class TransformError(Exception):
17    pass
18
19
20class NonUniformScalingError(TransformError):
21    pass
22
23
24class InsertTransformationError(TransformError):
25    pass
26
27
28def transform_thickness_and_extrusion_without_ocs(entity: 'DXFGraphic',
29                                                  m: Matrix44) -> None:
30    if entity.dxf.hasattr('thickness'):
31        thickness = entity.dxf.thickness
32        reflection = sign(thickness)
33        thickness = m.transform_direction(entity.dxf.extrusion * thickness)
34        entity.dxf.thickness = thickness.magnitude * reflection
35        entity.dxf.extrusion = thickness.normalize()
36    elif entity.dxf.hasattr('extrusion'):  # without thickness?
37        extrusion = m.transform_direction(entity.dxf.extrusion)
38        entity.dxf.extrusion = extrusion.normalize()
39
40
41def transform_extrusion(extrusion: 'Vertex', m: Matrix44) -> Tuple[Vec3, bool]:
42    """ Transforms the old `extrusion` vector into a new extrusion vector.
43    Returns the new extrusion vector and a boolean value: ``True`` if the new
44    OCS established by the new extrusion vector has a uniform scaled xy-plane,
45    else ``False``.
46
47    The new extrusion vector is perpendicular to plane defined by the
48    transformed x- and y-axis.
49
50    Args:
51        extrusion: extrusion vector of the old OCS
52        m: transformation matrix
53
54    Returns:
55
56    """
57    ocs = OCS(extrusion)
58    ocs_x_axis_in_wcs = ocs.to_wcs(X_AXIS)
59    ocs_y_axis_in_wcs = ocs.to_wcs(Y_AXIS)
60    x_axis, y_axis = m.transform_directions(
61        (ocs_x_axis_in_wcs, ocs_y_axis_in_wcs))
62
63    # Check for uniform scaled xy-plane:
64    is_uniform = math.isclose(x_axis.magnitude_square,
65                              y_axis.magnitude_square,
66                              abs_tol=1e-9)
67    new_extrusion = x_axis.cross(y_axis).normalize()
68    return new_extrusion, is_uniform
69
70
71class OCSTransform:
72    def __init__(self, extrusion: Vec3 = None, m: Matrix44 = None):
73        self.m = m
74        if extrusion is None:
75            self.old_ocs = None
76            self.scale_uniform = False
77            self.new_ocs = None
78        else:
79            self.old_ocs = OCS(extrusion)
80            new_extrusion, self.scale_uniform = transform_extrusion(
81                extrusion, m)
82            self.new_ocs = OCS(new_extrusion)
83
84    @property
85    def old_extrusion(self) -> Vec3:
86        return self.old_ocs.uz
87
88    @property
89    def new_extrusion(self) -> Vec3:
90        return self.new_ocs.uz
91
92    @classmethod
93    def from_ocs(cls, old: OCS, new: OCS, m: Matrix44) -> 'OCSTransform':
94        ocs = cls()
95        ocs.m = m
96        ocs.old_ocs = old
97        ocs.new_ocs = new
98        return ocs
99
100    def transform_length(self, length: 'Vertex', reflection=1.0) -> float:
101        """ Returns magnitude of `length` direction vector transformed from
102        old OCS into new OCS including `reflection` correction applied.
103        """
104        return self.m.transform_direction(
105            self.old_ocs.to_wcs(length)).magnitude * sign(reflection)
106
107    transform_scale_factor = transform_length
108
109    def transform_vertex(self, vertex: 'Vertex') -> Vec3:
110        """ Returns vertex transformed from old OCS into new OCS. """
111        return self.new_ocs.from_wcs(
112            self.m.transform(self.old_ocs.to_wcs(vertex)))
113
114    def transform_2d_vertex(self, vertex: 'Vertex', elevation: float) -> Vec2:
115        """ Returns 2D vertex transformed from old OCS into new OCS. """
116        v = Vec3(vertex).replace(z=elevation)
117        return self.new_ocs.from_wcs(
118            self.m.transform(self.old_ocs.to_wcs(v))).vec2
119
120    def transform_direction(self, direction: 'Vertex') -> Vec3:
121        """ Returns direction transformed from old OCS into new OCS. """
122        return self.new_ocs.from_wcs(
123            self.m.transform_direction(self.old_ocs.to_wcs(direction)))
124
125    def transform_angle(self, angle: float) -> float:
126        """ Returns angle (in radians) from old OCS transformed into new OCS.
127        """
128        return self.transform_direction(Vec3.from_angle(angle)).angle
129
130    def transform_deg_angle(self, angle: float) -> float:
131        """ Returns angle (in degrees) from old OCS transformed into new OCS.
132        """
133        return math.degrees(self.transform_angle(math.radians(angle)))
134