1# Purpose: dimension lines as composite entities build with basic dxf entities, but not the DIMENSION entity.
2# Created: 10.03.2010, 2018 adapted for ezdxf
3# Copyright (c) 2010-2018, Manfred Moitzi
4# License: MIT License
5"""
6Dimension lines as composite entities build with basic dxf entities, but not the DIMENSION entity.
7
8OBJECTS
9
10- LinearDimension
11- AngularDimension
12- ArcDimension
13- RadiusDimension
14
15PUBLIC MEMBERS
16
17dimstyles
18    dimstyle container
19
20    - new(name, kwargs) to create a new dimstyle
21    - get(name) to get a dimstyle, 'Default' if name does not exist
22    - setup(drawing) create Blocks and Layers in drawing
23"""
24from typing import Any, Dict, TYPE_CHECKING, Iterable, List, Tuple
25from math import radians, degrees, pi
26from abc import abstractmethod
27
28from ezdxf.math import Vec3, distance, lerp, ConstructionRay
29
30if TYPE_CHECKING:
31    from ezdxf.eztypes import Drawing, GenericLayoutType, Vertex
32
33DIMENSIONS_MIN_DISTANCE = 0.05
34DIMENSIONS_FLOATINGPOINT = '.'
35
36ANGLE_DEG = 180. / pi
37ANGLE_GRAD = 200. / pi
38ANGLE_RAD = 1.
39
40
41class DimStyle(dict):
42    """
43    DimStyle parameter struct, a dumb object just to store values
44
45    """
46    default_values = [
47        # tick block name, use setup to generate default blocks <dimblk> <dimblk1> <dimblk2>
48        ('tick', 'DIMTICK_ARCH'),
49        # scale factor for ticks-block <dimtsz> <dimasz>
50        ('tickfactor', 1.),
51        # tick2x means tick is drawn only for one side, insert tick a second
52        # time rotated about 180 degree, but only one time at the dimension line
53        # ends, this is useful for arrow-like ticks. hint: set dimlineext to 0. <none>
54        ('tick2x', False),
55        # dimension value scale factor, value = drawing-units * scale <dimlfac>
56        ('scale', 100.),
57        # round dimension value to roundval fractional digits <dimdec>
58        ('roundval', 0),
59        # round dimension value to half units, round 0.4, 0.6 to 0.5 <dimrnd>
60        ('roundhalf', False),
61        # dimension value text color <dimclrt>
62        ('textcolor', 7),
63        # dimension value text height <dimtxt>
64        ('height', .5),
65        # dimension text prefix and suffix like 'x=' ... ' cm' <dimpost>
66        ('prefix', ''),
67        ('suffix', ''),
68        # dimension value text style <dimtxsty>
69        ('style', 'OpenSansCondensed-Light'),
70        # default layer for whole dimension object
71        ('layer', 'DIMENSIONS'),
72        # dimension line color index (0 from layer) <dimclrd>
73        ('dimlinecolor', 7),
74        # dimension line extensions (in dimline direction, left and right) <dimdle>
75        ('dimlineext', .3),
76        # draw dimension value text `textabove` drawing-units above the
77        # dimension line <dimgap>
78        ('textabove', 0.2),
79        # switch extension line False=off, True=on <dimse1> <dimse2>
80        ('dimextline', True),
81        # dimension extension line color index (0 from layer) <dimclre>
82        ('dimextlinecolor', 5),
83        # gap between measure target point and end of extension line <dimexo>
84        ('dimextlinegap', 0.3)
85    ]
86
87    def __init__(self, name: str, **kwargs):
88        super().__init__(DimStyle.default_values)
89        # dimstyle name
90        self['name'] = name
91        self.update(kwargs)
92
93    def __getattr__(self, attr: str) -> Any:
94        return self[attr]
95
96    def __setattr__(self, attr: str, value: Any) -> None:
97        self[attr] = value
98
99
100class DimStyles:
101    """
102    DimStyle container
103
104    """
105
106    def __init__(self):
107        self._styles = {}  # type: Dict[str, DimStyle]
108        self.default = DimStyle('Default')
109
110        self.new(
111            "angle.deg",
112            scale=ANGLE_DEG,
113            suffix=str('°'),
114            roundval=0,
115            tick="DIMTICK_RADIUS",
116            tick2x=True,
117            dimlineext=0.,
118            dimextline=False
119        )
120        self.new(
121            "angle.grad",
122            scale=ANGLE_GRAD,
123            suffix='gon',
124            roundval=0,
125            tick="DIMTICK_RADIUS",
126            tick2x=True,
127            dimlineext=0.,
128            dimextline=False
129        )
130        self.new(
131            "angle.rad",
132            scale=ANGLE_RAD,
133            suffix='rad',
134            roundval=3,
135            tick="DIMTICK_RADIUS",
136            tick2x=True,
137            dimlineext=0.,
138            dimextline=False
139        )
140
141    def get(self, name: str) -> DimStyle:
142        """
143        Get DimStyle() object by name.
144        """
145        return self._styles.get(name, self.default)
146
147    def new(self, name: str, **kwargs) -> DimStyle:
148        """
149        Create a new dimstyle
150        """
151        style = DimStyle(name, **kwargs)
152        self._styles[name] = style
153        return style
154
155    @staticmethod
156    def setup(drawing: 'Drawing'):
157        """
158        Insert necessary definitions into drawing:
159
160            ticks: DIMTICK_ARCH, DIMTICK_DOT, DIMTICK_ARROW
161        """
162        # default pen assignment:
163        # 1 : 1.40mm - red
164        # 2 : 0.35mm - yellow
165        # 3 : 0.70mm - green
166        # 4 : 0.50mm - cyan
167        # 5 : 0.13mm - blue
168        # 6 : 1.00mm - magenta
169        # 7 : 0.25mm - white/black
170        # 8, 9 : 2.00mm
171        # >=10 : 1.40mm
172
173        dimcolor = {'color': dimstyles.default.dimextlinecolor, 'layer': 'BYBLOCK'}
174        color4 = {'color': 4, 'layer': 'BYBLOCK'}
175        color7 = {'color': 7, 'layer': 'BYBLOCK'}
176
177        block = drawing.blocks.new('DIMTICK_ARCH')
178        block.add_line(start=(0., +.5), end=(0., -.5), dxfattribs=dimcolor)
179        block.add_line(start=(-.2, -.2), end=(.2, +.2), dxfattribs=color4)
180
181        block = drawing.blocks.new('DIMTICK_DOT')
182        block.add_line(start=(0., .5), end=(0., -.5), dxfattribs=dimcolor)
183        block.add_circle(center=(0, 0), radius=.1, dxfattribs=color4)
184
185        block = drawing.blocks.new('DIMTICK_ARROW')
186
187        block.add_line(start=(0., .5), end=(0., -.50), dxfattribs=dimcolor)
188        block.add_solid([(0, 0), (.3, .05), (.3, -.05)], dxfattribs=color7)
189
190        block = drawing.blocks.new('DIMTICK_RADIUS')
191        block.add_solid([(0, 0), (.3, .05), (0.25, 0.), (.3, -.05)], dxfattribs=color7)
192
193
194dimstyles = DimStyles()  # use this factory to create new dimstyles
195
196
197class _DimensionBase:
198    """
199    Abstract base class for dimension lines.
200
201    """
202
203    def __init__(self, dimstyle: str, layer: str, roundval: int):
204        self.dimstyle = dimstyles.get(dimstyle)
205        self.layer = layer
206        self.roundval = roundval
207
208    def prop(self, property_name: str) -> Any:
209        """
210        Get dimension line properties by `property_name` with the possibility to override several properties.
211        """
212        if property_name == 'layer':
213            return self.layer if self.layer is not None else self.dimstyle.layer
214        elif property_name == 'roundval':
215            return self.roundval if self.roundval is not None else self.dimstyle.roundval
216        else:  # pass through self.dimstyle object DimStyle()
217            return self.dimstyle[property_name]
218
219    def format_dimtext(self, dimvalue: float) -> str:
220        """
221        Format the dimension text.
222        """
223        dimtextfmt = "%." + str(self.prop('roundval')) + "f"
224        dimtext = dimtextfmt % dimvalue
225        if DIMENSIONS_FLOATINGPOINT in dimtext:
226            # remove successional zeros
227            dimtext.rstrip('0')
228            # remove floating point as last char
229            dimtext.rstrip(DIMENSIONS_FLOATINGPOINT)
230        return self.prop('prefix') + dimtext + self.prop('suffix')
231
232    @abstractmethod
233    def render(self, layout: 'GenericLayoutType'):
234        pass
235
236
237class LinearDimension(_DimensionBase):
238    """
239    Simple straight dimension line with two or more measure points, build with basic DXF entities. This is NOT a dxf
240    dimension entity. And This is a 2D element, so all z-values will be ignored!
241
242    """
243
244    def __init__(self, pos: 'Vertex', measure_points: Iterable['Vertex'], angle: float = 0., dimstyle: str = 'Default',
245                 layer: str = None, roundval: int = None):
246        """
247        LinearDimension Constructor.
248
249        Args:
250            pos: location as (x, y) tuple of dimension line, line goes through this point
251            measure_points: list of points as (x, y) tuples to dimension (two or more)
252            angle: angle (in degree) of dimension line
253            dimstyle: dimstyle name, 'Default' - style is the default value
254            layer: dimension line layer, override the default value of dimstyle
255            roundval: count of decimal places
256
257        """
258        super().__init__(dimstyle, layer, roundval)
259        self.angle = angle
260        self.measure_points = list(measure_points)
261        self.text_override = [""] * self.section_count
262        self.dimlinepos = Vec3(pos)
263        self.layout = None
264
265    def set_text(self, section: int, text: str) -> None:
266        """
267        Set and override the text of the dimension text for the given dimension line section.
268        """
269        self.text_override[section] = text
270
271    def _setup(self) -> None:
272        """
273        Calc setup values and determines the point order of the dimension line points.
274        """
275        self.measure_points = [Vec3(point) for point in self.measure_points]  # type: List[Vec3]
276        dimlineray = ConstructionRay(self.dimlinepos, angle=radians(self.angle))  # Type: ConstructionRay
277        self.dimline_points = [self._get_point_on_dimline(point, dimlineray) for point in
278                               self.measure_points]  # type: List[Vec3]
279        self.point_order = self._indices_of_sorted_points(self.dimline_points)  # type: List[int]
280        self._build_vectors()
281
282    def _get_dimline_point(self, index: int) -> 'Vertex':
283        """
284        Get point on the dimension line, index runs left to right.
285        """
286        return self.dimline_points[self.point_order[index]]
287
288    def _get_section_points(self, section: int) -> Tuple[Vec3, Vec3]:
289        """
290        Get start and end point on the dimension line of dimension section.
291        """
292        return self._get_dimline_point(section), self._get_dimline_point(section + 1)
293
294    def _get_dimline_bounds(self) -> Tuple[Vec3, Vec3]:
295        """
296        Get the first and the last point of dimension line.
297        """
298        return self._get_dimline_point(0), self._get_dimline_point(-1)
299
300    @property
301    def section_count(self) -> int:
302        """ count of dimline sections """
303        return len(self.measure_points) - 1
304
305    @property
306    def point_count(self) -> int:
307        """ count of dimline points """
308        return len(self.measure_points)
309
310    def render(self, layout: 'GenericLayoutType') -> None:
311        """ build dimension line object with basic dxf entities """
312        self._setup()
313        self._draw_dimline(layout)
314        if self.prop('dimextline'):
315            self._draw_extension_lines(layout)
316        self._draw_text(layout)
317        self._draw_ticks(layout)
318
319    @staticmethod
320    def _indices_of_sorted_points(points: Iterable['Vertex']) -> List[int]:
321        """ get indices of points, for points sorted by x, y values """
322        indexed_points = [(point, idx) for idx, point in enumerate(points)]
323        indexed_points.sort()
324        return [idx for point, idx in indexed_points]
325
326    def _build_vectors(self) -> None:
327        """ build unit vectors, parallel and normal to dimension line """
328        point1, point2 = self._get_dimline_bounds()
329        self.parallel_vector = (Vec3(point2) - Vec3(point1)).normalize()
330        self.normal_vector = self.parallel_vector.orthogonal()
331
332    @staticmethod
333    def _get_point_on_dimline(point: 'Vertex', dimray: ConstructionRay) -> Vec3:
334        """ get the measure target point projection on the dimension line """
335        return dimray.intersect(dimray.orthogonal(point))
336
337    def _draw_dimline(self, layout: 'GenericLayoutType') -> None:
338        """ build dimension line entity """
339        start_point, end_point = self._get_dimline_bounds()
340
341        dimlineext = self.prop('dimlineext')
342        if dimlineext > 0:
343            start_point = start_point - (self.parallel_vector * dimlineext)
344            end_point = end_point + (self.parallel_vector * dimlineext)
345
346        attribs = {
347            'color': self.prop('dimlinecolor'),
348            'layer': self.prop('layer'),
349        }
350        layout.add_line(
351            start=start_point,
352            end=end_point,
353            dxfattribs=attribs,
354        )
355
356    def _draw_extension_lines(self, layout: 'GenericLayoutType') -> None:
357        """ build the extension lines entities """
358        dimextlinegap = self.prop('dimextlinegap')
359        attribs = {
360            'color': self.prop('dimlinecolor'),
361            'layer': self.prop('layer'),
362        }
363
364        for dimline_point, target_point in zip(self.dimline_points, self.measure_points):
365            if distance(dimline_point, target_point) > max(dimextlinegap, DIMENSIONS_MIN_DISTANCE):
366                direction_vector = (target_point - dimline_point).normalize()
367                target_point = target_point - (direction_vector * dimextlinegap)
368                layout.add_line(
369                    start=dimline_point,
370                    end=target_point,
371                    dxfattribs=attribs,
372                )
373
374    def _draw_text(self, layout: 'GenericLayoutType') -> None:
375        """ build the dimension value text entity """
376        attribs = {
377            'height': self.prop('height'),
378            'color': self.prop('textcolor'),
379            'layer': self.prop('layer'),
380            'rotation': self.angle,
381            'style': self.prop('style'),
382        }
383        for section in range(self.section_count):
384            dimvalue_text = self._get_dimvalue_text(section)
385            insert_point = self._get_text_insert_point(section)
386            layout.add_text(
387                text=dimvalue_text,
388                dxfattribs=attribs,
389            ).set_pos(insert_point, align='MIDDLE_CENTER')
390
391    def _get_dimvalue_text(self, section: int) -> str:
392        """ get the dimension value as text, distance from point1 to point2 """
393        override = self.text_override[section]
394        if len(override):
395            return override
396        point1, point2 = self._get_section_points(section)
397
398        dimvalue = distance(point1, point2) * self.prop('scale')
399        return self.format_dimtext(dimvalue)
400
401    def _get_text_insert_point(self, section: int) -> Vec3:
402        """ get the dimension value text insert point """
403        point1, point2 = self._get_section_points(section)
404        dist = self.prop('height') / 2. + self.prop('textabove')
405        return lerp(point1, point2) + (self.normal_vector * dist)
406
407    def _draw_ticks(self, layout: 'GenericLayoutType') -> None:
408        """ insert the dimension line ticks, (markers on the dimension line) """
409        attribs = {
410            'xscale': self.prop('tickfactor'),
411            'yscale': self.prop('tickfactor'),
412            'layer': self.prop('layer'),
413        }
414
415        def add_tick(index: int, rotate: bool = False) -> None:
416            """ build the insert-entity for the tick block """
417            attribs['rotation'] = self.angle + (180. if rotate else 0.)
418            layout.add_blockref(
419                insert=self._get_dimline_point(index),
420                name=self.prop('tick'),
421                dxfattribs=attribs,
422            )
423
424        if self.prop('tick2x'):
425            for index in range(0, self.point_count - 1):
426                add_tick(index, False)
427            for index in range(1, self.point_count):
428                add_tick(index, True)
429        else:
430            for index in range(self.point_count):
431                add_tick(index, False)
432
433
434class AngularDimension(_DimensionBase):
435    """
436    Draw an angle dimensioning line at dimline pos from start to end, dimension text is the angle build of the three
437    points start-center-end.
438
439    """
440    DEG = ANGLE_DEG
441    GRAD = ANGLE_GRAD
442    RAD = ANGLE_RAD
443
444    def __init__(self, pos: 'Vertex', center: 'Vertex', start: 'Vertex', end: 'Vertex',
445                 dimstyle: str = 'angle.deg', layer: str = None, roundval: int = None):
446        """
447        AngularDimension constructor.
448
449        Args:
450            pos: location as (x, y) tuple of dimension line, line goes through this point
451            center: center point as (x, y) tuple of angle
452            start: line from center to start is the first side of the angle
453            end: line from center to end is the second side of the angle
454            dimstyle: dimstyle name, 'Default' - style is the default value
455            layer: dimension line layer, override the default value of dimstyle
456            roundval: count of decimal places
457
458        """
459        super().__init__(dimstyle, layer, roundval)
460        self.dimlinepos = Vec3(pos)
461        self.center = Vec3(center)
462        self.start = Vec3(start)
463        self.end = Vec3(end)
464
465    def _setup(self) -> None:
466        """ setup calculation values """
467        self.pos_radius = distance(self.center, self.dimlinepos)  # type: float
468        self.radius = distance(self.center, self.start)  # type: float
469        self.start_vector = (self.start - self.center).normalize()  # type: Vec3
470        self.end_vector = (self.end - self.center).normalize()  # type: Vec3
471        self.start_angle = self.start_vector.angle  # type: float
472        self.end_angle = self.end_vector.angle  # type: float
473
474    def render(self, layout: 'GenericLayoutType') -> None:
475        """ build dimension line object with basic dxf entities """
476
477        self._setup()
478        self._draw_dimension_line(layout)
479        if self.prop('dimextline'):
480            self._draw_extension_lines(layout)
481        self._draw_dimension_text(layout)
482        self._draw_ticks(layout)
483
484    def _draw_dimension_line(self, layout: 'GenericLayoutType') -> None:
485        """ draw the dimension line from start- to endangle. """
486        layout.add_arc(
487            radius=self.pos_radius,
488            center=self.center,
489            start_angle=degrees(self.start_angle),
490            end_angle=degrees(self.end_angle),
491            dxfattribs={
492                'layer': self.prop('layer'),
493                'color': self.prop('dimlinecolor'),
494            }
495        )
496
497    def _draw_extension_lines(self, layout: 'GenericLayoutType') -> None:
498        """ build the extension lines entities """
499        for vector in [self.start_vector, self.end_vector]:
500            layout.add_line(
501                start=self._get_extline_start(vector),
502                end=self._get_extline_end(vector),
503                dxfattribs={
504                    'layer': self.prop('layer'),
505                    'color': self.prop('dimextlinecolor'),
506                }
507            )
508
509    def _get_extline_start(self, vector: Vec3) -> Vec3:
510        return self.center + (vector * self.prop('dimextlinegap'))
511
512    def _get_extline_end(self, vector: Vec3) -> Vec3:
513        return self.center + (vector * self.pos_radius)
514
515    def _draw_dimension_text(self, layout: 'GenericLayoutType') -> None:
516        attribs = {
517            'height': self.prop('height'),
518            'rotation': degrees((self.start_angle + self.end_angle) / 2 - pi / 2.),
519            'layer': self.prop('layer'),
520            'style': self.prop('style'),
521            'color': self.prop('textcolor'),
522        }
523        layout.add_text(
524            text=self._get_dimtext(),
525            dxfattribs=attribs,
526        ).set_pos(self._get_text_insert_point(), align='MIDDLE_CENTER')
527
528    def _get_text_insert_point(self) -> Vec3:
529        midvector = ((self.start_vector + self.end_vector) / 2.).normalize()
530        length = self.pos_radius + self.prop('textabove') + self.prop('height') / 2.
531        return self.center + (midvector * length)
532
533    def _draw_ticks(self, layout: 'GenericLayoutType') -> None:
534        attribs = {
535            'xscale': self.prop('tickfactor'),
536            'yscale': self.prop('tickfactor'),
537            'layer': self.prop('layer'),
538        }
539        for vector, mirror in [(self.start_vector, False), (self.end_vector, self.prop('tick2x'))]:
540            insert_point = self.center + (vector * self.pos_radius)
541            rotation = vector.angle + pi / 2.
542            attribs['rotation'] = degrees(rotation + (pi if mirror else 0.))
543            layout.add_blockref(
544                insert=insert_point,
545                name=self.prop('tick'),
546                dxfattribs=attribs,
547            )
548
549    def _get_dimtext(self) -> str:
550        # set scale = ANGLE_DEG for degrees (circle = 360 deg)
551        # set scale = ANGLE_GRAD for grad(circle = 400 grad)
552        # set scale = ANGLE_RAD for rad(circle = 2*pi)
553        angle = (self.end_angle - self.start_angle) * self.prop('scale')
554        return self.format_dimtext(angle)
555
556
557class ArcDimension(AngularDimension):
558    """
559    Arc is defined by start- and endpoint on arc and the center point, or by three points lying on the arc if acr3points
560    is True. Measured length goes from start- to endpoint. The dimension line goes through the dimlinepos.
561
562    """
563
564    def __init__(self, pos: 'Vertex', center: 'Vertex', start: 'Vertex', end: 'Vertex', arc3points: bool = False,
565                 dimstyle: str = 'Default', layer: str = None, roundval: int = None):
566        """
567        Args:
568            pos: location as (x, y) tuple of dimension line, line goes through this point
569            center: center point of arc
570            start: start point of arc
571            end: end point of arc
572            arc3points: if **True** arc is defined by three points on the arc (center, start, end)
573            dimstyle: dimstyle name, 'Default' - style is the default value
574            layer: dimension line layer, override the default value of dimstyle
575            roundval: count of decimal places
576
577        """
578        super().__init__(pos, center, start, end, dimstyle, layer, roundval)
579        self.arc3points = arc3points
580
581    def _setup(self) -> None:
582        super()._setup()
583        if self.arc3points:
584            self.center = center_of_3points_arc(self.center, self.start, self.end)
585
586    def _get_extline_start(self, vector: Vec3) -> Vec3:
587        return self.center + (vector * (self.radius + self.prop('dimextlinegap')))
588
589    def _get_extline_end(self, vector: Vec3) -> Vec3:
590        return self.center + (vector * self.pos_radius)
591
592    def _get_dimtext(self) -> str:
593        arc_length = (self.end_angle - self.start_angle) * self.radius * self.prop('scale')
594        return self.format_dimtext(arc_length)
595
596
597class RadialDimension(_DimensionBase):
598    """
599    Draw a radius dimension line from `target` in direction of `center` with length drawing units. RadiusDimension has
600    a special tick!!
601    """
602
603    def __init__(self, center: 'Vertex', target: 'Vertex', length: float = 1., dimstyle: str = 'Default',
604                 layer: str = None, roundval: int = None):
605        """
606        Args:
607            center: center point of radius
608            target: target point of radius
609            length: length of radius arrow (drawing length)
610            dimstyle: dimstyle name, 'Default' - style is the default value
611            layer: dimension line layer, override the default value of dimstyle
612            roundval: count of decimal places
613
614        """
615        super().__init__(dimstyle, layer, roundval)
616        self.center = Vec3(center)
617        self.target = Vec3(target)
618        self.length = float(length)
619
620    def _setup(self) -> None:
621        self.target_vector = (self.target - self.center).normalize()  # type: Vec3
622        self.radius = distance(self.center, self.target)  # type: float
623
624    def render(self, layout: 'GenericLayoutType') -> None:
625        """ build dimension line object with basic dxf entities """
626        self._setup()
627        self._draw_dimension_line(layout)
628        self._draw_dimension_text(layout)
629        self._draw_ticks(layout)
630
631    def _draw_dimension_line(self, layout: 'GenericLayoutType') -> None:
632        start_point = self.center + (self.target_vector * (self.radius - self.length))
633        layout.add_line(
634            start=start_point, end=self.target,
635            dxfattribs={
636                'color': self.prop('dimlinecolor'),
637                'layer': self.prop('layer'),
638            },
639        )
640
641    def _draw_dimension_text(self, layout: 'GenericLayoutType') -> None:
642        layout.add_text(
643            text=self._get_dimtext(),
644            dxfattribs={
645                'height': self.prop('height'),
646                'rotation': self.target_vector.angle_deg,
647                'layer': self.prop('layer'),
648                'style': self.prop('style'),
649                'color': self.prop('textcolor'),
650            }
651        ).set_pos(self._get_insert_point(), align='MIDDLE_RIGHT')
652
653    def _get_insert_point(self) -> Vec3:
654        return self.target - (self.target_vector * (self.length + self.prop('textabove')))
655
656    def _get_dimtext(self) -> str:
657        return self.format_dimtext(self.radius * self.prop('scale'))
658
659    def _draw_ticks(self, layout: 'GenericLayoutType') -> None:
660        layout.add_blockref(
661            insert=self.target,
662            name='DIMTICK_RADIUS',
663            dxfattribs={
664                'rotation': self.target_vector.angle_deg + 180,
665                'xscale': self.prop('tickfactor'),
666                'yscale': self.prop('tickfactor'),
667                'layer': self.prop('layer'),
668            }
669        )
670
671
672def center_of_3points_arc(point1: 'Vertex', point2: 'Vertex', point3: 'Vertex') -> Vec3:
673    """
674    Calc center point of 3 point arc. ConstructionCircle is defined by 3 points on the circle: point1, point2 and point3.
675    """
676    ray1 = ConstructionRay(point1, point2)
677    ray2 = ConstructionRay(point1, point3)
678    midpoint1 = lerp(point1, point2)
679    midpoint2 = lerp(point1, point3)
680    center_ray1 = ray1.orthogonal(midpoint1)
681    center_ray2 = ray2.orthogonal(midpoint2)
682    return center_ray1.intersect(center_ray2)
683