1# Purpose: The MText entity is a composite entity, consisting of basic TEXT entities.
2# Created: 09.03.2010, adapted 2018 for ezdxf
3# Copyright (c) 2010-2018, Manfred Moitzi
4# License: MIT License
5"""
6MText -- MultiLine-Text-Entity, created by simple TEXT-Entities.
7
8MTEXT was introduced in R13, so this is a replacement with multiple simple
9TEXT entities. Supports valign (TOP, MIDDLE, BOTTOM), halign (LEFT, CENTER,
10RIGHT), rotation for an arbitrary (!) angle and mirror.
11
12"""
13from typing import TYPE_CHECKING
14import math
15from .mixins import SubscriptAttributes
16import ezdxf
17from ezdxf.lldxf import const
18
19if TYPE_CHECKING:
20    from ezdxf.eztypes import Vertex, GenericLayoutType
21
22
23class MText(SubscriptAttributes):
24    """
25    MultiLine-Text buildup with simple Text-Entities.
26
27
28    Caution: align point is always the insert point, I don't need a second
29    alignpoint because horizontal alignment FIT, ALIGN, BASELINE_MIDDLE is not
30    supported.
31
32    linespacing -- linespacing in percent of height, 1.5 = 150% = 1+1/2 lines
33
34    supported align values:
35        'BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT'
36        'MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT'
37        'TOP_LEFT',    'TOP_CENTER',    'TOP_RIGHT'
38
39    """
40    MIRROR_X = const.MIRROR_X
41    MIRROR_Y = const.MIRROR_Y
42    TOP = const.TOP
43    MIDDLE = const.MIDDLE
44    BOTTOM = const.BOTTOM
45    LEFT = const.LEFT
46    CENTER = const.CENTER
47    RIGHT = const.RIGHT
48    VALID_ALIGN = frozenset([
49        'BOTTOM_LEFT',
50        'BOTTOM_CENTER',
51        'BOTTOM_RIGHT',
52        'MIDDLE_LEFT',
53        'MIDDLE_CENTER',
54        'MIDDLE_RIGHT',
55        'TOP_LEFT',
56        'TOP_CENTER',
57        'TOP_RIGHT',
58    ])
59
60    def __init__(self, text: str, insert: 'Vertex', linespacing: float = 1.5, **kwargs):
61        self.textlines = text.split('\n')
62        self.insert = insert
63        self.linespacing = linespacing
64        if 'align' in kwargs:
65            self.align = kwargs.get('align', 'TOP_LEFT').upper()
66        else:  # support for compatibility: valign, halign
67            halign = kwargs.get('halign', 0)
68            valign = kwargs.get('valign', 3)
69            self.align = const.TEXT_ALIGNMENT_BY_FLAGS.get((halign, valign), 'TOP_LEFT')
70
71        if self.align not in MText.VALID_ALIGN:
72            raise ezdxf.DXFValueError('Invalid align parameter: {}'.format(self.align))
73
74        self.height = kwargs.get('height', 1.0)
75        self.style = kwargs.get('style', 'STANDARD')
76        self.oblique = kwargs.get('oblique', 0.0)  # in degree
77        self.rotation = kwargs.get('rotation', 0.0)  # in degree
78        self.xscale = kwargs.get('xscale', 1.0)
79        self.mirror = kwargs.get('mirror', 0)  # renamed to text_generation_flag in ezdxf
80        self.layer = kwargs.get('layer', '0')
81        self.color = kwargs.get('color', const.BYLAYER)
82
83    @property
84    def lineheight(self) -> float:
85        """ Absolute linespacing in drawing units.
86        """
87        return self.height * self.linespacing
88
89    def render(self, layout: 'GenericLayoutType') -> None:
90        """ Create the DXF-TEXT entities.
91        """
92        textlines = self.textlines
93        if len(textlines) > 1:
94            if self.mirror & const.MIRROR_Y:
95                textlines.reverse()
96            for linenum, text in enumerate(textlines):
97                alignpoint = self._get_align_point(linenum)
98                layout.add_text(
99                    text,
100                    dxfattribs=self._dxfattribs(alignpoint),
101                )
102        elif len(textlines) == 1:
103            layout.add_text(
104                textlines[0],
105                dxfattribs=self._dxfattribs(self.insert),
106            )
107
108    def _get_align_point(self, linenum: int) -> 'Vertex':
109        """ Calculate the align point depending on the line number.
110        """
111        x = self.insert[0]
112        y = self.insert[1]
113        try:
114            z = self.insert[2]
115        except IndexError:
116            z = 0.
117        # rotation not respected
118
119        if self.align.startswith('TOP'):
120            y -= linenum * self.lineheight
121        elif self.align.startswith('MIDDLE'):
122            y0 = linenum * self.lineheight
123            fullheight = (len(self.textlines) - 1) * self.lineheight
124            y += (fullheight / 2) - y0
125        else:  # BOTTOM
126            y += (len(self.textlines) - 1 - linenum) * self.lineheight
127        return self._rotate((x, y, z))  # consider rotation
128
129    def _rotate(self, alignpoint: 'Vertex') -> 'Vertex':
130        """
131        Rotate alignpoint around insert point about rotation degrees.
132        """
133        dx = alignpoint[0] - self.insert[0]
134        dy = alignpoint[1] - self.insert[1]
135        beta = math.radians(self.rotation)
136        x = self.insert[0] + dx * math.cos(beta) - dy * math.sin(beta)
137        y = self.insert[1] + dy * math.cos(beta) + dx * math.sin(beta)
138        return round(x, 6), round(y, 6), alignpoint[2]
139
140    def _dxfattribs(self, alignpoint: 'Vertex') -> dict:
141        """
142        Build keyword arguments for TEXT entity creation.
143        """
144        halign, valign = const.TEXT_ALIGN_FLAGS.get(self.align)
145        return {
146            'insert': alignpoint,
147            'align_point': alignpoint,
148            'layer': self.layer,
149            'color': self.color,
150            'style': self.style,
151            'height': self.height,
152            'width': self.xscale,
153            'text_generation_flag': self.mirror,
154            'rotation': self.rotation,
155            'oblique': self.oblique,
156            'halign': halign,
157            'valign': valign,
158        }
159