# Created: 28.12.2018
# Copyright (C) 2018-2019, Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Tuple, Iterable, List, cast, Optional
import math
from ezdxf.math import Vector, Vec2, ConstructionRay, xround, ConstructionLine, ConstructionBox
from ezdxf.math import UCS, PassTroughUCS
from ezdxf.lldxf import const
from ezdxf.options import options
from ezdxf.lldxf.const import DXFValueError, DXFUndefinedBlockError
from ezdxf.tools import suppress_zeros
from ezdxf.render.arrows import ARROWS, connection_point
from ezdxf.dimstyleoverride import DimStyleOverride

if TYPE_CHECKING:
    from ezdxf.eztypes import Dimension, Vertex, Drawing, GenericLayoutType, Style


class TextBox(ConstructionBox):
    """
    Text boundaries representation.

    """

    def __init__(self, center: 'Vertex', width: float, height: float, angle: float, gap: float = 0):
        height += (2 * gap)
        super().__init__(center, width, height, angle)


PLUS_MINUS = '±'
_TOLERANCE_COMMON = r"\A{align};{txt}{{\H{fac:.2f}x;"
TOLERANCE_TEMPLATE1 = _TOLERANCE_COMMON + r"{tol}}}"
TOLERANCE_TEMPLATE2 = _TOLERANCE_COMMON + r"\S{upr}^ {lwr};}}"
LIMITS_TEMPLATE = r"{{\H{fac:.2f}x;\S{upr}^ {lwr};}}"


def OptionalVec2(v) -> Optional[Vec2]:
    if v is not None:
        return Vec2(v)
    else:
        return None


class BaseDimensionRenderer:
    """
    Base rendering class for DIMENSION entities.

    """

    def __init__(self, dimension: 'Dimension', ucs: 'UCS' = None, override: DimStyleOverride = None):
        # DXF document
        self.drawing = dimension.drawing  # type: Drawing

        # DIMENSION entity
        self.dimension = dimension  # type: Dimension

        self.dxfversion = self.drawing.dxfversion  # type: str
        self.supports_dxf_r2000 = self.dxfversion >= 'AC1015'  # type: bool
        self.supports_dxf_r2007 = self.dxfversion >= 'AC1021'  # type: bool
        # Target BLOCK of the graphical representation of the DIMENSION entity
        self.block = None  # type: GenericLayoutType

        # DimStyleOverride object, manages dimension style overriding
        if override:
            self.dim_style = override
        else:
            self.dim_style = DimStyleOverride(dimension)

        # User defined coordinate system for DIMENSION entity
        self.ucs = ucs or PassTroughUCS()
        self.requires_extrusion = self.ucs.uz != (0, 0, 1)  # type: bool

        # ezdxf specific attributes beyond DXF reference, therefore not stored in the DXF file (DSTYLE)
        # Some of these are just an rendering effect, which will be ignored by CAD applications if they modify the
        # DIMENSION entity

        # user location override as UCS coordinates, stored as text_midpoint in the DIMENSION entity
        self.user_location = OptionalVec2(self.dim_style.pop('user_location', None))

        # user location override relative to dimline center if True
        self.relative_user_location = self.dim_style.pop('relative_user_location', False)  # type: bool

        # shift text away from default text location - implemented as user location override without leader
        # shift text along in text direction
        self.text_shift_h = self.dim_style.pop('text_shift_h', 0.)  # type: float
        # shift text perpendicular to text direction
        self.text_shift_v = self.dim_style.pop('text_shift_v', 0.)  # type: float

        # suppress arrow rendering - only rendering is suppressed (rendering effect), all placing related calculations
        # are done without this settings. Used for multi point linear dimensions to avoid double rendering of non arrow
        # ticks.
        self.suppress_arrow1 = self.dim_style.pop('suppress_arrow1', False)  # type: bool
        self.suppress_arrow2 = self.dim_style.pop('suppress_arrow2', False)  # type: bool
        # end of ezdxf specific attributes

        # ---------------------------------------------
        # GENERAL PROPERTIES
        # ---------------------------------------------
        self.default_color = self.dimension.dxf.color  # type: int
        self.default_layer = self.dimension.dxf.layer  # type: str

        # ezdxf creates ALWAYS attachment points in the text center.
        self.text_attachment_point = 5  # type: int # fixed for ezdxf rendering

        # ignored by ezdxf
        self.horizontal_direction = self.dimension.get_dxf_attrib('horizontal_direction', None)  # type: bool

        get = self.dim_style.get
        # overall scaling of DIMENSION entity
        self.dim_scale = get('dimscale', 1)  # type: float
        if self.dim_scale == 0:
            self.dim_scale = 1

        # Controls drawing of circle or arc center marks and centerlines, for DIMDIAMETER and DIMRADIUS, the center
        # mark is drawn only if you place the dimension line outside the circle or arc.
        # 0 = No center marks or lines are drawn
        # <0 = Center lines are drawn
        # >0 = Center marks are drawn
        self.dim_center_marks = get('dimcen', 0)  # type: int  # not supported yet

        # ---------------------------------------------
        # TEXT
        # ---------------------------------------------
        # dimension measurement factor
        self.dim_measurement_factor = get('dimlfac', 1)  # type: float
        self.text_style_name = get('dimtxsty', options.default_dimension_text_style)  # type: str
        self.text_style = self.drawing.styles.get(self.text_style_name)  # type: Style
        self.text_height = self.char_height * self.dim_scale  # type: float
        self.text_width_factor = self.text_style.get_dxf_attrib('width', 1.)  # type: float
        # text_gap: gap between dimension line an dimension text
        self.text_gap = get('dimgap', 0.625) * self.dim_scale  # type: float
        # user defined text rotation - overrides everything
        self.user_text_rotation = self.dimension.get_dxf_attrib('text_rotation', None)  # type: float
        # calculated text rotation
        self.text_rotation = self.user_text_rotation  # type: float
        self.text_color = get('dimclrt', self.default_color)  # type: int
        self.text_round = get('dimrnd', None)  # type: float
        self.text_decimal_places = get('dimdec', None)  # type: int

        # Controls the suppression of zeros in the primary unit value.
        # Values 0-3 affect feet-and-inch dimensions only and are not supported
        # 4 (Bit 3) = Suppresses leading zeros in decimal dimensions (for example, 0.5000 becomes .5000)
        # 8 (Bit 4) = Suppresses trailing zeros in decimal dimensions (for example, 12.5000 becomes 12.5)
        # 12 (Bit 3+4) = Suppresses both leading and trailing zeros (for example, 0.5000 becomes .5)
        self.text_suppress_zeros = get('dimzin', 0)  # type: int

        dimdsep = self.dim_style.get('dimdsep', 0)
        self.text_decimal_separator = ',' if dimdsep == 0 else chr(dimdsep)  # type: str
        self.text_format = self.dim_style.get('dimpost', '<>')  # type: str
        self.text_fill = self.dim_style.get('dimtfill', 0)  # type: int # 0= None, 1=Background, 2=DIMTFILLCLR
        self.text_fill_color = self.dim_style.get('dimtfillclr', 1)  # type: int
        self.text_box_fill_scale = 1.1

        # text_halign = 0: center; 1: left; 2: right; 3: above ext1; 4: above ext2
        self.text_halign = get('dimjust', 0)  # type: int

        # text_valign = 0: center; 1: above; 2: farthest away?; 3: JIS?; 4: below (2, 3 ignored by ezdxf)
        self.text_valign = get('dimtad', 0)  # type: int

        # Controls the vertical position of dimension text above or below the dimension line, when DIMTAD = 0.
        # The magnitude of the vertical offset of text is the product of the text height (+gap?) and DIMTVP.
        # Setting DIMTVP to 1.0 is equivalent to setting DIMTAD = 1.
        self.text_vertical_position = get('dimtvp', 0.)  # type: float  # not supported yet

        self.text_movement_rule = get('dimtmove', 2)  # type: int # move text freely
        if self.text_movement_rule == 0:
            # moves the dimension line with dimension text and makes no sense for ezdxf (just set `base` argument)
            self.text_movement_rule = 2

        # requires a leader?
        self.text_has_leader = self.user_location is not None and self.text_movement_rule == 1  # type: bool

        # text_rotation=0 if dimension text is 'inside', ezdxf defines 'inside' as at the default text location
        self.text_inside_horizontal = get('dimtih', 0)  # type: bool

        # text_rotation=0 if dimension text is 'outside', ezdxf defines 'outside' as NOT at the default text location
        self.text_outside_horizontal = get('dimtoh', 0)  # type: bool

        # force text location 'inside', even if the text should be moved 'outside'
        self.force_text_inside = bool(get('dimtix', 0))  # type: bool

        # how dimension text and arrows are arranged when space is not sufficient to place both 'inside'
        # 0 = Places both text and arrows outside extension lines
        # 1 = Moves arrows first, then text
        # 2 = Moves text first, then arrows
        # 3 = Moves either text or arrows, whichever fits best
        self.text_fitting_rule = get('dimatfit', 2)  # type: int  # not supported yet - ezdxf behaves like 2

        # units for all dimension types except Angular.
        # 1 = Scientific
        # 2 = Decimal
        # 3 = Engineering
        # 4 = Architectural (always displayed stacked)
        # 5 = Fractional (always displayed stacked)
        self.text_length_unit = get('dimlunit', 2)  # type: int  # not supported yet - ezdxf behaves like 2

        # fraction format when DIMLUNIT is set to 4 (Architectural) or 5 (Fractional).
        # 0 = Horizontal stacking
        # 1 = Diagonal stacking
        # 2 = Not stacked (for example, 1/2)
        self.text_fraction_format = get('dimfrac', 0)  # type: int  # not supported

        # units format for angular dimensions
        # 0 = Decimal degrees
        # 1 = Degrees/minutes/seconds (not supported) same as 0
        # 2 = Grad
        # 3 = Radians
        self.text_angle_unit = get('dimaunit', 0)  # type: int

        # text_outside is only True if really placed outside of default text location
        # remark: user defined text location is always outside per definition (not by real location)
        self.text_outside = False

        # calculated or overridden dimension text location
        self.text_location = None  # type: Vec2

        # bounding box of dimension text including border space
        self.text_box = None  # type: TextBox

        # formatted dimension text
        self.text = ""

        # True if dimension text doesn't fit between extension lines
        self.is_wide_text = False

        # ---------------------------------------------
        # ARROWS & TICKS
        # ---------------------------------------------
        self.tick_size = get('dimtsz') * self.dim_scale
        if self.tick_size > 0:
            # use oblique strokes as 'arrows', disables usual 'arrows' and user defined blocks
            self.arrow1_name, self.arrow2_name = None, None  # type: str
            # tick size is per definition double the size of arrow size
            # adjust arrow size to reuse the 'oblique' arrow block
            self.arrow_size = self.tick_size * 2  # type: float
        else:
            # arrow name or block name if user defined arrow
            self.arrow1_name, self.arrow2_name = self.dim_style.get_arrow_names()  # type: str
            self.arrow_size = get('dimasz') * self.dim_scale  # type: float

        # Suppresses arrowheads if not enough space is available inside the extension lines.
        # Only if force_text_inside is True
        self.suppress_arrow_heads = get('dimsoxd', 0)  # type: bool # not supported yet

        # ---------------------------------------------
        # DIMENSION LINE
        # ---------------------------------------------
        self.dim_line_color = get('dimclrd', self.default_color)  # type: int

        # dimension line extension, along the dimension line direction ('left' and 'right')
        self.dim_line_extension = get('dimdle', 0.) * self.dim_scale  # type: float
        self.dim_linetype = get('dimltype', None)  # type: str
        self.dim_lineweight = get('dimlwd', const.LINEWEIGHT_BYBLOCK)  # type: int

        # suppress first part of the dimension line
        self.suppress_dim1_line = get('dimsd1', 0)  # type: bool

        # suppress second part of the dimension line
        self.suppress_dim2_line = get('dimsd2', 0)  # type: bool

        # Controls whether a dimension line is drawn between the extension lines even when the text is placed outside.
        # For radius and diameter dimensions (when DIMTIX is off), draws a dimension line inside the circle or arc and
        # places the text, arrowheads, and leader outside.
        # 0 = no dimension line
        # 1 = draw dimension line
        self.dim_line_if_text_outside = get('dimtofl', 1)  # type: int  # not supported yet - ezdxf behaves like 1

        # ---------------------------------------------
        # EXTENSION LINES
        # ---------------------------------------------
        self.ext_line_color = get('dimclre', self.default_color)
        self.ext1_linetype_name = get('dimltex1', None)  # type: str
        self.ext2_linetype_name = get('dimltex2', None)  # type: str
        self.ext_lineweight = get('dimlwe', const.LINEWEIGHT_BYBLOCK)
        self.suppress_ext1_line = get('dimse1', 0)  # type: bool
        self.suppress_ext2_line = get('dimse2', 0)  # type: bool

        # extension of extension line above the dimension line, in extension line direction
        # in most cases perpendicular to dimension line (oblique!)
        self.ext_line_extension = get('dimexe', 0.) * self.dim_scale  # type: float

        # distance of extension line from the measurement point in extension line direction
        self.ext_line_offset = get('dimexo', 0.) * self.dim_scale  # type: float

        # fixed length extension line, leenght above dimension line is still self.ext_line_extension
        self.ext_line_fixed = get('dimflxon', 0)  # type: bool

        # length below the dimension line:
        self.ext_line_length = get('dimflx', self.ext_line_extension) * self.dim_scale  # type: float

        # ---------------------------------------------
        # TOLERANCES & LIMITS
        # ---------------------------------------------
        # appends tolerances to dimension text. Setting DIMTOL to on turns DIMLIM off.
        self.dim_tolerance = get('dimtol', 0)  # type: bool
        # generates dimension limits as the default text. Setting DIMLIM to On turns DIMTOL off.
        self.dim_limits = get('dimlim', 0)  # type: bool

        if self.dim_tolerance:
            self.dim_limits = 0

        if self.dim_limits:
            self.dim_tolerance = 0

        # scale factor for the text height of fractions and tolerance values relative to the dimension text height
        self.tol_text_scale_factor = get('dimtfac', .5)
        self.tol_line_spacing = 1.35  # default MTEXT line spacing for tolerances (BricsCAD)
        # sets the minimum (or lower) tolerance limit for dimension text when DIMTOL or DIMLIM is on.
        # DIMTM accepts signed values. If DIMTOL is on and DIMTP and DIMTM are set to the same value, a tolerance value
        # is drawn. If DIMTM and DIMTP values differ, the upper tolerance is drawn above the lower, and a plus sign is
        # added to the DIMTP value if it is positive. For DIMTM, the program uses the negative of the value you enter
        # (adding a minus sign if you specify a positive number and a plus sign if you specify a negative number).
        self.tol_minimum = get('dimtm', 0)  # type: float

        # Sets the maximum (or upper) tolerance limit for dimension text when DIMTOL or DIMLIM is on. DIMTP accepts
        # signed values. If DIMTOL is on and DIMTP and DIMTM are set to the same value, a tolerance value is drawn.
        # If DIMTM and DIMTP values differ, the upper tolerance is drawn above the lower and a plus sign is added to
        # the DIMTP value if it is positive.
        self.tol_maximum = get('dimtp', 0)  # type: float

        # number of decimal places to display in tolerance values
        self.tol_decimal_places = get('dimtdec', 4)  # type: int

        # vertical justification for tolerance values relative to the nominal dimension text
        # 0 = Bottom
        # 1 = Middle
        # 2 = Top
        self.tol_valign = get('dimtolj', 0)  # type: int

        # same as DIMZIN for tolerances (self.text_suppress_zeros)
        self.tol_suppress_zeros = get('dimtzin', 0)  # type: int
        self.tol_text = None
        self.tol_text_height = 0.
        self.tol_text_upper = None
        self.tol_text_lower = None
        self.tol_char_height = self.char_height * self.tol_text_scale_factor * self.dim_scale
        # tolerances
        if self.dim_tolerance:
            # single tolerance value +/- value
            if self.tol_minimum == self.tol_maximum:
                self.tol_text = PLUS_MINUS + self.format_tolerance_text(abs(self.tol_maximum))
                self.tol_text_height = self.tol_char_height
                self.tol_text_width = self.tolerance_text_width(len(self.tol_text))
            else:  # 2 stacked values: +upper tolerance <above> -lower tolerance
                self.tol_text_upper = sign_char(self.tol_maximum) + self.format_tolerance_text(
                    abs(self.tol_maximum))
                self.tol_text_lower = sign_char(self.tol_minimum * -1) + self.format_tolerance_text(
                    abs(self.tol_minimum))
                # requires 2 text lines
                self.tol_text_height = self.tol_char_height + (self.tol_text_height * self.tol_line_spacing)
                self.tol_text_width = self.tolerance_text_width(max(len(self.tol_text_upper), len(self.tol_text_lower)))
            # reset text height
            self.text_height = max(self.text_height, self.tol_text_height)

        elif self.dim_limits:
            self.tol_text = None  # always None for limits
            # limits text is always 2 stacked numbers and requires actual measurement
            self.tol_text_upper = None  # text for upper limit
            self.tol_text_lower = None  # text for lower limit
            self.tol_text_height = self.tol_char_height + (self.tol_text_height * self.tol_line_spacing)
            self.tol_text_width = None  # requires actual measurement
            self.text_height = max(self.text_height, self.tol_text_height)

    @property
    def text_inside(self):
        return not self.text_outside

    def render(self, block: 'GenericLayoutType'):  # interface definition
        self.block = block
        # tolerance requires MTEXT support, switch of rendering of tolerances and limits
        if not self.supports_dxf_r2000:
            self.dim_tolerance = 0
            self.dim_limits = 0


    @property
    def char_height(self) -> float:
        """
        Unscaled (self.dim_scale) character height defined by text style or DIMTXT.
        Hint: Use self.text_height for proper scaled text height in drawing units.

        """
        height = self.text_style.get_dxf_attrib('height', 0)  # type: float
        if height == 0:  # variable text height (not fixed)
            height = self.dim_style.get('dimtxt', 1.)
        return height

    def text_width(self, text: str) -> float:
        """
        Return width of `text` in drawing units.

        """
        char_width = self.text_height * self.text_width_factor  # type: float
        return len(text) * char_width

    def tolerance_text_width(self, count: int) -> float:
        """
        Return width of `count` characters in drawing units.

        """
        return self.tol_text_height * self.text_width_factor * count

    def default_attributes(self) -> dict:
        """
        Returns default DXF attributes as dict.

        """
        return {
            'layer': self.default_layer,  # type: str
            'color': self.default_color,  # type: int
        }

    def wcs(self, point: 'Vertex') -> Vector:
        """
        Transform `point` in UCS coordinates into WCS coordinates.

        """
        return self.ucs.to_wcs(point)

    def ocs(self, point: 'Vertex') -> Vector:
        """
        Transform `point` in UCS coordinates into OCS coordinates.

        """
        return self.ucs.to_ocs(point)

    def to_ocs_angle(self, angle: float) -> float:
        """
        Transform `angle` from UCS to OCS.

        """
        return self.ucs.to_ocs_angle_deg(angle)

    def text_override(self, measurement: float) -> str:
        """
        Create dimension text for `measurement` in drawing units and applies text overriding properties.

        """
        text = self.dimension.dxf.text  # type: str
        if text == ' ':  # suppress text
            return ''
        elif text == '' or text == '<>':  # measured distance
            return self.format_text(measurement)
        else:  # user override
            return text

    def format_text(self, value: float) -> str:
        """
        Rounding and text formatting of `value`, removes leading and trailing zeros if necessary.

        """
        return format_text(
            value,
            self.text_round,
            self.text_decimal_places,
            self.text_suppress_zeros,
            self.text_decimal_separator,
            self.text_format,
        )

    def compile_mtext(self) -> str:
        text = self.text
        if self.dim_tolerance:
            align = max(int(self.tol_valign), 0)
            align = min(align, 2)
            if self.tol_text is None:
                text = TOLERANCE_TEMPLATE2.format(
                    align=align,
                    txt=text,
                    fac=self.tol_text_scale_factor,
                    upr=self.tol_text_upper,
                    lwr=self.tol_text_lower,
                )
            else:
                text = TOLERANCE_TEMPLATE1.format(
                    align=align,
                    txt=text,
                    fac=self.tol_text_scale_factor,
                    tol=self.tol_text,
                )
        elif self.dim_limits:
            text = LIMITS_TEMPLATE.format(
                upr=self.tol_text_upper,
                lwr=self.tol_text_lower,
                fac=self.tol_text_scale_factor,
            )
        return text

    def format_tolerance_text(self, value: float) -> str:
        """
        Rounding and text formatting of tolerance `value`, removes leading and trailing zeros if necessary.

        """
        return format_text(
            value=value,
            dimrnd=None,
            dimdec=self.tol_decimal_places,
            dimzin=self.tol_suppress_zeros,
            dimdsep=self.text_decimal_separator,
        )

    def location_override(self, location: 'Vertex', leader=False, relative=False) -> None:
        """
        Set user defined dimension text location. ezdxf defines a user defined location per definition as 'outside'.

        Args:
            location: text midpoint
            leader: use leader or not (movement rules)
            relative: is location absolut (in UCS) or relative to dimension line center.

        """
        self.dim_style.set_location(location, leader, relative)
        self.user_location = Vec2(location)
        self.text_movement_rule = 1 if leader else 2
        self.relative_user_location = relative
        self.text_outside = True

    def add_line(self, start: 'Vertex', end: 'Vertex', dxfattribs: dict = None, remove_hidden_lines=False) -> None:
        """
        Add a LINE entity to the dimension BLOCK. Removes parts of the line hidden by dimension text if
        `remove_hidden_lines` is True.

        Args:
            start: start point of line
            end: end point of line
            dxfattribs: additional or overridden DXF attributes
            remove_hidden_lines: removes parts of the line hidden by dimension text if True

        """

        def order(a: Vec2, b: Vec2) -> Tuple[Vec2, Vec2]:
            if (start - a).magnitude < (start - b).magnitude:
                return a, b
            else:
                return b, a

        attribs = self.default_attributes()
        if dxfattribs:
            attribs.update(dxfattribs)
        text_box = self.text_box
        wcs = self.ucs.to_wcs
        if remove_hidden_lines and (text_box is not None):
            start_inside = int(text_box.is_inside(start))
            end_inside = int(text_box.is_inside(end))
            inside = start_inside + end_inside
            if inside == 2:  # start and end inside text_box
                return  # do not draw line
            elif inside == 1:  # one point inside text_box
                intersection_points = text_box.intersect(ConstructionLine(start, end))
                # one point inside one point outside -> one intersection point
                p1 = intersection_points[0]
                p2 = start if start_inside else end
                self.block.add_line(wcs(p1), wcs(p2), dxfattribs=attribs)
                return
            else:
                intersection_points = text_box.intersect(ConstructionLine(start, end))
                if len(intersection_points) == 2:
                    # sort intersection points by distance to start point
                    p1, p2 = order(intersection_points[0], intersection_points[1])
                    # line[start-p1] - gap - line[p2-end]
                    self.block.add_line(wcs(start), wcs(p1), dxfattribs=attribs)
                    self.block.add_line(wcs(p2), wcs(end), dxfattribs=attribs)
                    return
                # else: fall trough
        self.block.add_line(wcs(start), wcs(end), dxfattribs=attribs)

    def add_blockref(self, name: str, insert: 'Vertex', rotation: float = 0,
                     scale: float = 1., dxfattribs: dict = None) -> None:
        """
        Add block references and standard arrows to the dimension BLOCK.

        Args:
            name: block or arrow name
            insert: insertion point in UCS
            rotation: rotation angle in degrees in UCS (x-axis is 0 degrees)
            scale: scaling factor for x- and y-direction
            dxfattribs: additional or overridden DXF attributes

        """
        attribs = self.default_attributes()
        insert = self.ocs(insert)
        rotation = self.to_ocs_angle(rotation)
        if self.requires_extrusion:
            attribs['extrusion'] = self.ucs.uz
        if name in ARROWS:  # generates automatically BLOCK definitions for arrows if needed
            if dxfattribs:
                attribs.update(dxfattribs)
            self.block.add_arrow_blockref(name, insert=insert, size=scale, rotation=rotation, dxfattribs=attribs)
        else:
            if name not in self.drawing.blocks:
                raise DXFUndefinedBlockError('Undefined block: "{}"'.format(name))
            attribs['rotation'] = rotation
            if scale != 1.:
                attribs['xscale'] = scale
                attribs['yscale'] = scale
            if dxfattribs:
                attribs.update(dxfattribs)
            self.block.add_blockref(name, insert=insert, dxfattribs=attribs)

    def add_text(self, text: str, pos: Vector, rotation: float, dxfattribs: dict = None) -> None:
        """
        Add TEXT (DXF R12) or MTEXT (DXF R2000+) entity to the dimension BLOCK.

        Args:
            text: text as string
            pos: insertion location in UCS
            rotation: rotation angle in degrees in UCS (x-axis is 0 degrees)
            dxfattribs: additional or overridden DXF attributes

        """
        attribs = self.default_attributes()
        attribs['style'] = self.text_style_name
        attribs['color'] = self.text_color
        if self.requires_extrusion:
            attribs['extrusion'] = self.ucs.uz

        if self.supports_dxf_r2000:
            text_direction = self.ucs.to_wcs(Vec2.from_deg_angle(rotation)) - self.ucs.origin
            attribs['text_direction'] = text_direction
            attribs['char_height'] = self.text_height
            attribs['insert'] = self.wcs(pos)
            attribs['attachment_point'] = self.text_attachment_point

            if self.supports_dxf_r2007:
                if self.text_fill:
                    attribs['box_fill_scale'] = self.text_box_fill_scale
                    attribs['bg_fill_color'] = self.text_fill_color
                    attribs['bg_fill'] = 3 if self.text_fill == 1 else 1

            if dxfattribs:
                attribs.update(dxfattribs)
            self.block.add_mtext(text, dxfattribs=attribs)
        else:
            attribs['rotation'] = self.ucs.to_ocs_angle_deg(rotation)
            attribs['height'] = self.text_height
            if dxfattribs:
                attribs.update(dxfattribs)
            dxftext = self.block.add_text(text, dxfattribs=attribs)
            dxftext.set_pos(self.ocs(pos), align='MIDDLE_CENTER')

    def add_defpoints(self, points: Iterable['Vertex']) -> None:
        """
        Add POINT entities at layer 'DEFPOINTS' for all points in `points`.

        """
        attribs = {
            'layer': 'DEFPOINTS',
        }
        for point in points:
            self.block.add_point(self.wcs(point), dxfattribs=attribs)

    def add_leader(self, p1: Vec2, p2: Vec2, p3: Vec2, dxfattribs: dict = None):
        """
        Add simple leader line from p1 to p2 to p3.

        Args:
            p1: target point
            p2: first text point
            p3: second text point
            dxfattribs: DXF attribute

        """
        self.add_line(p1, p2, dxfattribs)
        self.add_line(p2, p3, dxfattribs)

    def transform_ucs_to_wcs(self) -> None:
        """
        Transforms dimension definition points into WCS or if required into OCS.

        Can not be called in __init__(), because inherited classes may be need unmodified values.

        """

        def from_ucs(attr, func):
            point = self.dimension.get_dxf_attrib(attr)
            self.dimension.set_dxf_attrib(attr, func(point))

        from_ucs('defpoint', self.wcs)
        from_ucs('defpoint2', self.wcs)
        from_ucs('defpoint3', self.wcs)
        from_ucs('text_midpoint', self.ocs)
        self.dimension.dxf.angle = self.ucs.to_ocs_angle_deg(self.dimension.dxf.angle)

    def finalize(self) -> None:
        self.transform_ucs_to_wcs()


def order_leader_points(p1: Vec2, p2: Vec2, p3: Vec2) -> Tuple[Vec2, Vec2]:
    if (p1 - p2).magnitude > (p1 - p3).magnitude:
        return p3, p2
    else:
        return p2, p3


class LinearDimension(BaseDimensionRenderer):
    """
    Linear dimension line renderer, used for horizontal, vertical, rotated and aligned DIMENSION entities.

    Args:
        dimension: DXF entity DIMENSION
        ucs: user defined coordinate system
        override: dimension style override management object

    """

    def __init__(self, dimension: 'Dimension', ucs: 'UCS' = None, override: 'DimStyleOverride' = None):
        super().__init__(dimension, ucs, override)
        self.oblique_angle = self.dimension.get_dxf_attrib('oblique_angle', 90)  # type: float
        self.dim_line_angle = self.dimension.get_dxf_attrib('angle', 0)  # type: float
        self.dim_line_angle_rad = math.radians(self.dim_line_angle)  # type: float
        self.ext_line_angle = self.dim_line_angle + self.oblique_angle  # type: float
        self.ext_line_angle_rad = math.radians(self.ext_line_angle)  # type: float

        # text is aligned to dimension line
        self.text_rotation = self.dim_line_angle  # type: float
        if self.text_halign in (3, 4):  # text above extension line, is always aligned with extension lines
            self.text_rotation = self.ext_line_angle

        self.ext1_line_start = Vec2(self.dimension.dxf.defpoint2)
        self.ext2_line_start = Vec2(self.dimension.dxf.defpoint3)

        ext1_ray = ConstructionRay(self.ext1_line_start, angle=self.ext_line_angle_rad)
        ext2_ray = ConstructionRay(self.ext2_line_start, angle=self.ext_line_angle_rad)
        dim_line_ray = ConstructionRay(self.dimension.dxf.defpoint, angle=self.dim_line_angle_rad)

        self.dim_line_start = dim_line_ray.intersect(ext1_ray)  # type: Vec2
        self.dim_line_end = dim_line_ray.intersect(ext2_ray)  # type: Vec2
        self.dim_line_center = self.dim_line_start.lerp(self.dim_line_end)  # type: Vec2

        if self.dim_line_start == self.dim_line_end:
            self.dim_line_vec = Vec2.from_angle(self.dim_line_angle_rad)
        else:
            self.dim_line_vec = (self.dim_line_end - self.dim_line_start).normalize()  # type: Vec2

        # set dimension defpoint to expected location - 3D vertex required!
        self.dimension.dxf.defpoint = Vector(self.dim_line_start)

        self.measurement = (self.dim_line_end - self.dim_line_start).magnitude  # type: float
        self.text = self.text_override(self.measurement * self.dim_measurement_factor)  # type: str

        # only for linear dimension in multi point mode
        self.multi_point_mode = override.pop('multi_point_mode', False)

        # 1 .. move wide text up
        # 2 .. move wide text down
        # None .. ignore
        self.move_wide_text = override.pop('move_wide_text', None)  # type: bool

        # actual text width in drawing units
        self.dim_text_width = 0  # type: float

        # arrows
        self.required_arrows_space = 2 * self.arrow_size + self.text_gap  # type: float
        self.arrows_outside = self.required_arrows_space > self.measurement  # type: bool

        # text location and rotation
        if self.text:
            # text width and required space
            self.dim_text_width = self.text_width(self.text)  # type: float
            if self.dim_tolerance:
                self.dim_text_width += self.tol_text_width

            elif self.dim_limits:
                # limits show the upper and lower limit of the measurement as stacked values
                # and with the size of tolerances
                measurement = self.measurement * self.dim_measurement_factor
                self.measurement_upper_limit = measurement + self.tol_maximum
                self.measurement_lower_limit = measurement - self.tol_minimum
                self.tol_text_upper = self.format_tolerance_text(self.measurement_upper_limit)
                self.tol_text_lower = self.format_tolerance_text(self.measurement_lower_limit)
                self.tol_text_width = self.tolerance_text_width(max(len(self.tol_text_upper), len(self.tol_text_lower)))

                # only limits are displayed so:
                self.dim_text_width = self.tol_text_width

            if self.multi_point_mode:
                # ezdxf has total control about vertical text position in multi point mode
                self.text_vertical_position = 0.

            if self.text_valign == 0 and abs(self.text_vertical_position) < 0.7:
                # vertical centered text needs also space for arrows
                required_space = self.dim_text_width + 2 * self.arrow_size
            else:
                required_space = self.dim_text_width
            self.is_wide_text = required_space > self.measurement

            if not self.force_text_inside:
                # place text outside if wide text and not forced inside
                self.text_outside = self.is_wide_text
            elif self.is_wide_text and self.text_halign < 3:
                # center wide text horizontal
                self.text_halign = 0

            # use relative text shift to move wide text up or down in multi point mode
            if self.multi_point_mode and self.is_wide_text and self.move_wide_text:
                shift_value = self.text_height + self.text_gap
                if self.move_wide_text == 1:  # move text up
                    self.text_shift_v = shift_value
                    if self.vertical_placement == -1:  # text below dimension line
                        # shift again
                        self.text_shift_v += shift_value
                elif self.move_wide_text == 2:  # move text down
                    self.text_shift_v = -shift_value
                    if self.vertical_placement == 1:  # text above dimension line
                        # shift again
                        self.text_shift_v -= shift_value

            # get final text location - no altering after this line
            self.text_location = self.get_text_location()  # type: Vec2

            # text rotation override
            rotation = self.text_rotation  # type: float
            if self.user_text_rotation is not None:
                rotation = self.user_text_rotation
            elif self.text_outside and self.text_outside_horizontal:
                rotation = 0
            elif self.text_inside and self.text_inside_horizontal:
                rotation = 0
            self.text_rotation = rotation

            self.text_box = TextBox(
                center=self.text_location,
                width=self.dim_text_width,
                height=self.text_height,
                angle=self.text_rotation,
                gap=self.text_gap * .75
            )
            if self.text_has_leader:
                p1, p2, *_ = self.text_box.corners
                self.leader1, self.leader2 = order_leader_points(self.dim_line_center, p1, p2)
                # not exact what BricsCAD (AutoCAD) expect, but close enough
                self.dimension.dxf.text_midpoint = self.leader1
            else:
                # write final text location into DIMENSION entity
                self.dimension.dxf.text_midpoint = self.text_location

    @property
    def has_relative_text_movement(self):
        return bool(self.text_shift_h or self.text_shift_v)

    def apply_text_shift(self, location: Vec2, text_rotation: float) -> Vec2:
        """
        Add `self.text_shift_h` and `sel.text_shift_v` to point `location`, shifting along and perpendicular to
        text orientation defined by `text_rotation`

        Args:
            location: location point
            text_rotation: text rotation in degrees

        Returns: new location

        """
        shift_vec = Vec2((self.text_shift_h, self.text_shift_v))
        location += shift_vec.rotate(text_rotation)
        return location

    def render(self, block: 'GenericLayoutType') -> None:
        """
        Main method to create dimension geometry as basic DXF entities in the associated BLOCK layout.

        Args:
            block: target BLOCK for rendering

        """
        # call required to setup some requirements
        super().render(block)

        # add extension line 1
        if not self.suppress_ext1_line:
            above_ext_line1 = self.text_halign == 3
            start, end = self.extension_line_points(self.ext1_line_start, self.dim_line_start, above_ext_line1)
            self.add_extension_line(start, end, linetype=self.ext1_linetype_name)

        # add extension line 2
        if not self.suppress_ext2_line:
            above_ext_line2 = self.text_halign == 4
            start, end = self.extension_line_points(self.ext2_line_start, self.dim_line_end, above_ext_line2)
            self.add_extension_line(start, end, linetype=self.ext2_linetype_name)

        # add arrow symbols (block references), also adjust dimension line start and end point
        dim_line_start, dim_line_end = self.add_arrows()

        # add dimension line
        self.add_dimension_line(dim_line_start, dim_line_end)

        # add measurement text as last entity to see text fill properly
        if self.text:
            if self.supports_dxf_r2000:
                text = self.compile_mtext()
            else:
                text = self.text
            self.add_measurement_text(text, self.text_location, self.text_rotation)
            if self.text_has_leader:
                self.add_leader(self.dim_line_center, self.leader1, self.leader2)

        # add POINT entities at definition points
        self.add_defpoints([self.dim_line_start, self.ext1_line_start, self.ext2_line_start])

    def get_text_location(self) -> Vec2:
        """
        Get text midpoint in UCS from user defined location or default text location.

        """
        # apply relative text shift as user location override without leader
        if self.has_relative_text_movement:
            location = self.default_text_location()
            location = self.apply_text_shift(location, self.text_rotation)
            self.location_override(location)

        if self.user_location is not None:
            location = self.user_location
            if self.relative_user_location:
                location = self.dim_line_center + location
            # define overridden text location as outside
            self.text_outside = True
        else:
            location = self.default_text_location()

        return location

    def default_text_location(self) -> Vec2:
        """
        Calculate default text location in UCS based on `self.text_halign`, `self.text_valign` and `self.text_outside`

        """
        start = self.dim_line_start
        end = self.dim_line_end
        halign = self.text_halign
        # positions the text above and aligned with the first/second extension line
        if halign in (3, 4):
            # horizontal location
            hdist = self.text_gap + self.text_height / 2.
            hvec = self.dim_line_vec * hdist
            location = (start if halign == 3 else end) - hvec
            # vertical location
            vdist = self.ext_line_extension + self.dim_text_width / 2.
            location += Vec2.from_deg_angle(self.ext_line_angle).normalize(vdist)
        else:
            # relocate outside text to center location
            if self.text_outside:
                halign = 0

            if halign == 0:
                location = self.dim_line_center  # center of dimension line
            else:
                hdist = self.dim_text_width / 2. + self.arrow_size + self.text_gap
                if halign == 1:  # positions the text next to the first extension line
                    location = start + (self.dim_line_vec * hdist)
                else:  # positions the text next to the second extension line
                    location = end - (self.dim_line_vec * hdist)

            if self.text_outside:  # move text up
                vdist = self.ext_line_extension + self.text_gap + self.text_height / 2.
            else:
                # distance from extension line to text midpoint
                vdist = self.text_vertical_distance()
            location += self.dim_line_vec.orthogonal().normalize(vdist)

        return location

    def add_arrows(self) -> Tuple[Vec2, Vec2]:
        """
        Add arrows or ticks to dimension.

        Returns: dimension line connection points

        """
        attribs = {
            'color': self.dim_line_color,
        }
        start = self.dim_line_start
        end = self.dim_line_end
        outside = self.arrows_outside
        arrow1 = not self.suppress_arrow1
        arrow2 = not self.suppress_arrow2
        if self.tick_size > 0.:  # oblique stroke, but double the size
            if arrow1:
                self.add_blockref(
                    ARROWS.oblique,
                    insert=start,
                    rotation=self.dim_line_angle,
                    scale=self.tick_size * 2,
                    dxfattribs=attribs,
                )
            if arrow2:
                self.add_blockref(
                    ARROWS.oblique,
                    insert=end,
                    rotation=self.dim_line_angle,
                    scale=self.tick_size * 2,
                    dxfattribs=attribs,
                )
        else:
            scale = self.arrow_size
            start_angle = self.dim_line_angle + 180.
            end_angle = self.dim_line_angle
            if outside:
                start_angle, end_angle = end_angle, start_angle

            if arrow1:
                self.add_blockref(self.arrow1_name, insert=start, scale=scale, rotation=start_angle,
                                  dxfattribs=attribs)  # reverse
            if arrow2:
                self.add_blockref(self.arrow2_name, insert=end, scale=scale, rotation=end_angle, dxfattribs=attribs)

            if not outside:
                # arrows inside extension lines: adjust connection points for the remaining dimension line
                if arrow1:
                    start = connection_point(self.arrow1_name, start, scale, start_angle)
                if arrow2:
                    end = connection_point(self.arrow2_name, end, scale, end_angle)
            else:
                # add additional extension lines to arrows placed outside of dimension extension lines
                self.add_arrow_extension_lines()
        return start, end

    def add_arrow_extension_lines(self):
        """
        Add extension lines to arrows placed outside of dimension extension lines. Called by `self.add_arrows()`.

        """

        def has_arrow_extension(name: str) -> bool:
            return (name is not None) and (name in ARROWS) and (name not in ARROWS.ORIGIN_ZERO)

        attribs = {
            'color': self.dim_line_color,
        }
        start = self.dim_line_start
        end = self.dim_line_end
        arrow_size = self.arrow_size

        if not self.suppress_arrow1 and has_arrow_extension(self.arrow1_name):
            self.add_line(
                start - self.dim_line_vec * arrow_size,
                start - self.dim_line_vec * (2 * arrow_size),
                dxfattribs=attribs,
            )

        if not self.suppress_arrow2 and has_arrow_extension(self.arrow2_name):
            self.add_line(
                end + self.dim_line_vec * arrow_size,
                end + self.dim_line_vec * (2 * arrow_size),
                dxfattribs=attribs,
            )

    def add_measurement_text(self, dim_text: str, pos: Vec2, rotation: float) -> None:
        """
        Add measurement text to dimension BLOCK.

        Args:
            dim_text: dimension text
            pos: text location
            rotation: text rotation in degrees

        """
        attribs = {
            'color': self.text_color,
        }
        self.add_text(dim_text, pos=Vector(pos), rotation=rotation, dxfattribs=attribs)

    def add_dimension_line(self, start: 'Vertex', end: 'Vertex') -> None:
        """
        Add dimension line to dimension BLOCK, adds extension DIMDLE if required, and uses DIMSD1 or DIMSD2 to suppress
        first or second part of dimension line. Removes line parts hidden by dimension text.

        Args:
            start: dimension line start
            end: dimension line end

        """
        extension = self.dim_line_vec * self.dim_line_extension
        if self.arrow1_name is None or ARROWS.has_extension_line(self.arrow1_name):
            start = start - extension
        if self.arrow2_name is None or ARROWS.has_extension_line(self.arrow2_name):
            end = end + extension

        attribs = {
            'color': self.dim_line_color
        }
        if self.dim_linetype is not None:
            attribs['linetype'] = self.dim_linetype

        if self.supports_dxf_r2000:
            attribs['lineweight'] = self.dim_lineweight

        if self.suppress_dim1_line or self.suppress_dim2_line:
            if not self.suppress_dim1_line:
                self.add_line(start, self.dim_line_center, dxfattribs=attribs, remove_hidden_lines=True)
            if not self.suppress_dim2_line:
                self.add_line(self.dim_line_center, end, dxfattribs=attribs, remove_hidden_lines=True)
        else:
            self.add_line(start, end, dxfattribs=attribs, remove_hidden_lines=True)

    def extension_line_points(self, start: Vec2, end: Vec2, text_above_extline=False) -> Tuple[Vec2, Vec2]:
        """
        Adjust start and end point of extension line by dimension variables DIMEXE, DIMEXO, DIMEXFIX, DIMEXLEN.

        Args:
            start: start point of extension line (measurement point)
            end: end point at dimension line
            text_above_extline: True if text is above and aligned with extension line

        Returns: adjusted start and end point

        """
        if start == end:
            direction = Vec2.from_deg_angle(self.ext_line_angle)
        else:
            direction = (end - start).normalize()
        if self.ext_line_fixed:
            start = end - (direction * self.ext_line_length)
        else:
            start = start + direction * self.ext_line_offset
        extension = self.ext_line_extension
        if text_above_extline:
            extension += self.dim_text_width
        end = end + direction * extension
        return start, end

    def add_extension_line(self, start: 'Vertex', end: 'Vertex', linetype: str = None) -> None:
        """
        Add extension lines from dimension line to measurement point.

        """
        attribs = {
            'color': self.ext_line_color
        }
        if linetype is not None:
            attribs['linetype'] = linetype

        # lineweight requires DXF R2000 or later
        if self.supports_dxf_r2000:
            attribs['lineweight'] = self.ext_lineweight

        self.add_line(start, end, dxfattribs=attribs)

    @property
    def vertical_placement(self) -> float:
        """
        Returns vertical placement of dimension text as 1 for above, 0 for center and -1 for below dimension line.

        """
        if self.text_valign == 0:
            return 0
        elif self.text_valign == 4:
            return -1
        else:
            return 1

    def text_vertical_distance(self) -> float:
        """
        Returns the vertical distance for dimension line to text midpoint. Positive values are above the line, negative
        values are below the line.

        """
        if self.text_valign == 0:
            return self.text_height * self.text_vertical_position
        else:
            return (self.text_height / 2. + self.text_gap) * self.vertical_placement


class DimensionRenderer:
    def dispatch(self, override: 'DimStyleOverride', ucs: 'UCS') -> BaseDimensionRenderer:
        dimension = override.dimension
        dim_type = dimension.dim_type

        if dim_type in (0, 1):
            return self.linear(dimension, ucs, override)
        elif dim_type == 2:
            return self.angular(dimension, ucs, override)
        elif dim_type == 3:
            return self.diameter(dimension, ucs, override)
        elif dim_type == 4:
            return self.radius(dimension, ucs, override)
        elif dim_type == 5:
            return self.angular3p(dimension, ucs, override)
        elif dim_type == 6:
            return self.ordinate(dimension, ucs, override)
        else:
            raise DXFValueError("Unknown DIMENSION type: {}".format(dim_type))

    def linear(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
        """
        Call renderer for linear dimension lines: horizontal, vertical and rotated
        """
        return LinearDimension(dimension, ucs, override)

    def angular(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
        raise NotImplemented

    def diameter(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
        raise NotImplemented

    def radius(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
        raise NotImplemented

    def angular3p(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
        raise NotImplemented

    def ordinate(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
        raise NotImplemented


def format_text(value: float, dimrnd: float = None, dimdec: int = None, dimzin: int = 0, dimdsep: str = '.',
                dimpost: str = '<>') -> str:
    if dimrnd is not None:
        value = xround(value, dimrnd)

    if dimdec is None:
        fmt = "{:f}"
        dimzin = dimzin | 8  # remove pending zeros for undefined decimal places, '{:f}'.format(0) -> '0.000000'
    else:
        fmt = "{:." + str(dimdec) + "f}"
    text = fmt.format(value)

    leading = bool(dimzin & 4)
    pending = bool(dimzin & 8)
    text = suppress_zeros(text, leading, pending)
    if dimdsep != '.':
        text = text.replace('.', dimdsep)
    if dimpost:
        if '<>' in dimpost:
            fmt = dimpost.replace('<>', '{}', 1)
            text = fmt.format(text)
        else:
            raise DXFValueError('Invalid dimpost string: "{}"'.format(dimpost))
    return text


CAN_SUPPRESS_ARROW1 = {
    ARROWS.dot,
    ARROWS.dot_small,
    ARROWS.dot_blank,
    ARROWS.origin_indicator,
    ARROWS.origin_indicator_2,
    ARROWS.dot_smallblank,
    ARROWS.none,
    ARROWS.oblique,
    ARROWS.box_filled,
    ARROWS.box,
    ARROWS.integral,
    ARROWS.architectural_tick,
}


def sign_char(value: float) -> str:
    if value < 0.:
        return '-'
    elif value > 0:
        return '+'
    else:
        return ' '


def sort_projected_points(points: Iterable['Vertex'], angle: float = 0) -> List[Vec2]:
    direction = Vec2.from_deg_angle(angle)
    projected_vectors = [(direction.project(Vec2(p)), p) for p in points]
    return [p for projection, p in sorted(projected_vectors)]


def multi_point_linear_dimension(
        layout: 'GenericLayoutType',
        base: 'Vertex',
        points: Iterable['Vertex'],
        angle: float = 0,
        ucs: 'UCS' = None,
        avoid_double_rendering: bool = True,
        dimstyle: str = 'EZDXF',
        override: dict = None,
        dxfattribs: dict = None,
        discard=False) -> None:
    """
    Creates multiple DIMENSION entities for each point pair in `points`. Measurement points will be sorted by appearance
    on the dimension line vector.

    Args:
        layout: target layout (model space, paper space or block)
        base: base point, any point on the dimension line vector will do
        points: iterable of measurement points
        angle: dimension line rotation in degrees (0=horizontal, 90=vertical)
        ucs: user defined coordinate system
        avoid_double_rendering: removes first extension line and arrow of following DIMENSION entity
        dimstyle: dimension style name
        override: dictionary of overridden dimension style attributes
        dxfattribs: DXF attributes for DIMENSION entities
        discard: discard rendering result for friendly CAD applications like BricsCAD to get a native and likely better
                 rendering result. (does not work with AutoCAD)

    """

    def suppress_arrow1(dimstyle_override) -> bool:
        arrow_name1, arrow_name2 = dimstyle_override.get_arrow_names()
        if (arrow_name1 is None) or (arrow_name1 in CAN_SUPPRESS_ARROW1):
            return True
        else:
            return False

    points = sort_projected_points(points, angle)
    base = Vec2(base)
    override = override or {}
    override['dimtix'] = 1  # do not place measurement text outside
    override['dimtvp'] = 0  # do not place measurement text outside
    override['multi_point_mode'] = True
    # 1 .. move wide text up; 2 .. move wide text down; None .. ignore
    # moving text down, looks best combined with text fill bg: DIMTFILL = 1
    move_wide_text = 1
    _suppress_arrow1 = False
    first_run = True

    for p1, p2 in zip(points[:-1], points[1:]):
        _override = dict(override)
        _override['move_wide_text'] = move_wide_text
        if avoid_double_rendering and not first_run:
            _override['dimse1'] = 1
            _override['suppress_arrow1'] = _suppress_arrow1

        style = layout.add_linear_dim(
            Vector(base),
            Vector(p1),
            Vector(p2),
            angle=angle,
            dimstyle=dimstyle,
            override=_override,
            dxfattribs=dxfattribs,
        )
        if first_run:
            _suppress_arrow1 = suppress_arrow1(style)

        renderer = cast(LinearDimension, style.render(ucs, discard=discard))
        if renderer.is_wide_text:
            # after wide text switch moving direction
            if move_wide_text == 1:
                move_wide_text = 2
            else:
                move_wide_text = 1
        else:  # reset to move text up
            move_wide_text = 1
        first_run = False
