# created: 2019-01-03 # Copyright (c) 2019 Manfred Moitzi # License: MIT License from typing import TYPE_CHECKING, Iterable from ezdxf.math import Vec2, Shape2d from .forms import open_arrow, arrow2 if TYPE_CHECKING: from ezdxf.eztypes import Vertex, GenericLayoutType DEFAULT_ARROW_ANGLE = 18.924644 DEFAULT_BETA = 45. # The base Arrow is for the 'Right' side ->| of the Dimension oriented, reverse is the 'Left' side |<-. class BaseArrow: def __init__(self, vertices: Iterable['Vertex']): self.shape = Shape2d(vertices) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): pass def place(self, insert: 'Vertex', angle: float): self.shape.rotate(angle) self.shape.translate(insert) class NoneStroke(BaseArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): super().__init__([Vec2(insert)]) class ObliqueStroke(BaseArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): self.size = size s2 = size / 2 # shape = [center, lower left, upper right] super().__init__([Vec2((-s2, -s2)), Vec2((s2, s2))]) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): layout.add_line(start=self.shape[0], end=self.shape[1], dxfattribs=dxfattribs) class ArchTick(ObliqueStroke): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): width = self.size * .15 if layout.dxfversion > 'AC1009': dxfattribs['const_width'] = width layout.add_lwpolyline(self.shape, format='xy', dxfattribs=dxfattribs) else: dxfattribs['default_start_width'] = width dxfattribs['default_end_width'] = width layout.add_polyline2d(self.shape, dxfattribs=dxfattribs) class ClosedArrowBlank(BaseArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): super().__init__(open_arrow(size, angle=DEFAULT_ARROW_ANGLE)) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): if layout.dxfversion > 'AC1009': polyline = layout.add_lwpolyline( points=self.shape, dxfattribs=dxfattribs) else: polyline = layout.add_polyline2d( points=self.shape, dxfattribs=dxfattribs) polyline.close(True) class ClosedArrow(ClosedArrowBlank): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): super().render(layout, dxfattribs) end_point = self.shape[0].lerp(self.shape[2]) layout.add_line(start=self.shape[1], end=end_point, dxfattribs=dxfattribs) class ClosedArrowFilled(ClosedArrow): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): layout.add_solid( points=self.shape, dxfattribs=dxfattribs, ) class _OpenArrow(BaseArrow): def __init__(self, arrow_angle: float, insert: 'Vertex', size: float = 1.0, angle: float = 0): points = list(open_arrow(size, angle=arrow_angle)) points.append((-1, 0)) super().__init__(points) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): if layout.dxfversion > 'AC1009': layout.add_lwpolyline(points=self.shape[:-1], dxfattribs=dxfattribs) else: layout.add_polyline2d(points=self.shape[:-1], dxfattribs=dxfattribs) layout.add_line(start=self.shape[1], end=self.shape[-1], dxfattribs=dxfattribs) class OpenArrow(_OpenArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): super().__init__(DEFAULT_ARROW_ANGLE, insert, size, angle) class OpenArrow30(_OpenArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): super().__init__(30, insert, size, angle) class OpenArrow90(_OpenArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): super().__init__(90, insert, size, angle) class Circle(BaseArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): self.radius = size / 2 # shape = [center point, connection point] super().__init__([ Vec2((0, 0)), Vec2((-self.radius, 0)), Vec2((-size, 0)), ]) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): layout.add_circle(center=self.shape[0], radius=self.radius, dxfattribs=dxfattribs) class Origin(Circle): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): super().render(layout, dxfattribs) layout.add_line(start=self.shape[0], end=self.shape[2], dxfattribs=dxfattribs) class CircleBlank(Circle): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): super().render(layout, dxfattribs) layout.add_line(start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs) class Origin2(Circle): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): layout.add_circle(center=self.shape[0], radius=self.radius, dxfattribs=dxfattribs) layout.add_circle(center=self.shape[0], radius=self.radius / 2, dxfattribs=dxfattribs) layout.add_line(start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs) class DotSmall(Circle): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): dxfattribs['closed'] = True center = self.shape[0] d = Vec2((self.radius / 2, 0)) p1 = center - d p2 = center + d if layout.dxfversion > 'AC1009': dxfattribs['const_width'] = self.radius layout.add_lwpolyline([(p1, 1), (p2, 1)], format='vb', dxfattribs=dxfattribs) else: dxfattribs['default_start_width'] = self.radius dxfattribs['default_end_width'] = self.radius polyline = layout.add_polyline2d(points=[p1, p2], dxfattribs=dxfattribs) polyline[0].dxf.bulge = 1 polyline[1].dxf.bulge = 1 class Dot(DotSmall): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): layout.add_line(start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs) super().render(layout, dxfattribs) class Box(BaseArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): # shape = [lower_left, lower_right, upper_right, upper_left, connection point] s2 = size / 2 super().__init__([ Vec2((-s2, -s2)), Vec2((+s2, -s2)), Vec2((+s2, +s2)), Vec2((-s2, +s2)), Vec2((-s2, 0)), Vec2((-size, 0)), ]) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): if layout.dxfversion > 'AC1009': polyline = layout.add_lwpolyline(points=self.shape[0:4], dxfattribs=dxfattribs) else: polyline = layout.add_polyline2d(points=self.shape[0:4], dxfattribs=dxfattribs) polyline.close(True) layout.add_line(start=self.shape[4], end=self.shape[5], dxfattribs=dxfattribs) class BoxFilled(Box): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): def solid_order(): v = self.shape.vertices return [v[0], v[1], v[3], v[2]] layout.add_solid(points=solid_order(), dxfattribs=dxfattribs) layout.add_line(start=self.shape[4], end=self.shape[5], dxfattribs=dxfattribs) class Integral(BaseArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): self.radius = size * .3535534 self.angle = angle # shape = [center, left_center, right_center] super().__init__([ Vec2((0, 0)), Vec2((-self.radius, 0)), Vec2((self.radius, 0)), ]) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): angle = self.angle layout.add_arc(center=self.shape[1], radius=self.radius, start_angle=-90 + angle, end_angle=angle, dxfattribs=dxfattribs) layout.add_arc(center=self.shape[2], radius=self.radius, start_angle=90 + angle, end_angle=180 + angle, dxfattribs=dxfattribs) class DatumTriangle(BaseArrow): REVERSE_ANGLE = 180 def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): d = .577350269 * size # tan(30) # shape = [upper_corner, lower_corner, connection_point] super().__init__([ Vec2((0, d)), Vec2((0, -d)), Vec2((-size, 0)), ]) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): if layout.dxfversion > 'AC1009': polyline = layout.add_lwpolyline(points=self.shape, dxfattribs=dxfattribs) else: polyline = layout.add_polyline2d(points=self.shape, dxfattribs=dxfattribs) polyline.close(True) class DatumTriangleFilled(DatumTriangle): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): layout.add_solid(points=self.shape, dxfattribs=dxfattribs) class _EzArrow(BaseArrow): def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0): points = list(arrow2(size, angle=DEFAULT_ARROW_ANGLE)) points.append((-1, 0)) super().__init__(points) self.place(insert, angle) def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): if layout.dxfversion > 'AC1009': polyline = layout.add_lwpolyline(self.shape[:-1], dxfattribs=dxfattribs) else: polyline = layout.add_polyline2d(self.shape[:-1], dxfattribs=dxfattribs) polyline.close(True) class EzArrowBlank(_EzArrow): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): super().render(layout, dxfattribs) layout.add_line(start=self.shape[-2], end=self.shape[-1], dxfattribs=dxfattribs) class EzArrow(_EzArrow): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): super().render(layout, dxfattribs) layout.add_line(start=self.shape[1], end=self.shape[-1], dxfattribs=dxfattribs) class EzArrowFilled(_EzArrow): def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): points = self.shape.vertices layout.add_solid([points[0], points[1], points[3], points[2]], dxfattribs=dxfattribs) layout.add_line(start=self.shape[-2], end=self.shape[-1], dxfattribs=dxfattribs) class _Arrows: closed_filled = "" dot = "DOT" dot_small = "DOTSMALL" dot_blank = "DOTBLANK" origin_indicator = "ORIGIN" origin_indicator_2 = "ORIGIN2" open = "OPEN" right_angle = "OPEN90" open_30 = "OPEN30" closed = "CLOSED" dot_smallblank = "SMALL" none = "NONE" oblique = "OBLIQUE" box_filled = "BOXFILLED" box = "BOXBLANK" closed_blank = "CLOSEDBLANK" datum_triangle_filled = "DATUMFILLED" datum_triangle = "DATUMBLANK" integral = "INTEGRAL" architectural_tick = "ARCHTICK" # ezdxf special arrows ez_arrow = "EZ_ARROW" ez_arrow_blank = "EZ_ARROW_BLANK" ez_arrow_filled = "EZ_ARROW_FILLED" CLASSES = { closed_filled: ClosedArrowFilled, dot: Dot, dot_small: DotSmall, dot_blank: CircleBlank, origin_indicator: Origin, origin_indicator_2: Origin2, open: OpenArrow, right_angle: OpenArrow90, open_30: OpenArrow30, closed: ClosedArrow, dot_smallblank: Circle, none: NoneStroke, oblique: ObliqueStroke, box_filled: BoxFilled, box: Box, closed_blank: ClosedArrowBlank, datum_triangle: DatumTriangle, datum_triangle_filled: DatumTriangleFilled, integral: Integral, architectural_tick: ArchTick, ez_arrow: EzArrow, ez_arrow_blank: EzArrowBlank, ez_arrow_filled: EzArrowFilled, } # arrows with origin at dimension line start/end ORIGIN_ZERO = { architectural_tick, oblique, dot_small, dot_smallblank, integral, none, } __acad__ = { closed_filled, dot, dot_small, dot_blank, origin_indicator, origin_indicator_2, open, right_angle, open_30, closed, dot_smallblank, none, oblique, box_filled, box, closed_blank, datum_triangle, datum_triangle_filled, integral, architectural_tick } __ezdxf__ = { ez_arrow, ez_arrow_blank, ez_arrow_filled, } __all_arrows__ = __acad__ | __ezdxf__ EXTENSIONS_ALLOWED = { architectural_tick, oblique, none, dot_smallblank, integral, dot_small, } def is_acad_arrow(self, item: str) -> bool: return item.upper() in self.__acad__ def is_ezdxf_arrow(self, item: str) -> bool: return item.upper() in self.__ezdxf__ def has_extension_line(self, name): return name in self.EXTENSIONS_ALLOWED def __contains__(self, item: str) -> bool: if item is None: return False return item.upper() in self.__all_arrows__ def create_block(self, blocks, name: str): block_name = self.block_name(name) if block_name not in blocks: block = blocks.new(block_name) arrow = self.arrow_shape(name, insert=(0, 0), size=1, rotation=0) arrow.render(block, dxfattribs={'color': 0, 'linetype': 'BYBLOCK'}) return block_name def block_name(self, name): if not self.is_acad_arrow(name): # common BLOCK definition return name.upper() # e.g. Dimension.dxf.bkl = 'EZ_ARROW' == Insert.dxf.name elif name == "": # special AutoCAD arrow symbols 'CLOSED_FILLED' has no name # ezdxf uses blocks for ALL arrows, but '_' (closed filled) as block name? return "_CLOSEDFILLED" # Dimension.dxf.bkl = '' != Insert.dxf.name = '_CLOSED_FILLED' else: # special AutoCAD arrow symbols have leading '_' as common practice! return '_' + name.upper() # Dimension.dxf.bkl = 'DOT' != Insert.dxf.name = '_DOT' def arrow_name(self, block_name: str) -> str: if block_name.startswith('_'): name = block_name[1:].upper() if name == 'CLOSEDFILLED': return '' elif self.is_acad_arrow(name): return name return block_name def insert_arrow(self, layout: 'GenericLayoutType', name: str, insert: 'Vertex', size: float = 1., rotation: float = 0, dxfattribs: dict = None) -> Vec2: block_name = self.create_block(layout.drawing.blocks, name) dxfattribs = dict(dxfattribs) if dxfattribs else {} # copy attribs dxfattribs['rotation'] = rotation dxfattribs['xscale'] = size dxfattribs['yscale'] = size layout.add_blockref(block_name, insert=insert, dxfattribs=dxfattribs) return connection_point(name, insert=insert, scale=size, rotation=rotation) def render_arrow(self, layout: 'GenericLayoutType', name: str, insert: 'Vertex', size: float = 1., rotation: float = 0, dxfattribs: dict = None) -> Vec2: dxfattribs = dxfattribs or {} arrow = self.arrow_shape(name, insert, size, rotation) arrow.render(layout, dxfattribs) return connection_point(name, insert=insert, scale=size, rotation=rotation) def arrow_shape(self, name: str, insert: 'Vertex', size: float, rotation: float) -> BaseArrow: # size depending shapes if name == self.dot_small: size *= .25 elif name == self.dot_smallblank: size *= .5 cls = self.CLASSES[name] return cls(insert, size, rotation) def connection_point(arrow_name: str, insert: 'Vertex', scale: float = 1, rotation: float = 0) -> Vec2: insert = Vec2(insert) if arrow_name in _Arrows.ORIGIN_ZERO: return insert else: return insert - Vec2.from_deg_angle(rotation, scale) ARROWS = _Arrows()