arrows.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # created: 2019-01-03
  2. # Copyright (c) 2019 Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, Iterable
  5. from ezdxf.math import Vec2, Shape2d
  6. from .forms import open_arrow, arrow2
  7. if TYPE_CHECKING:
  8. from ezdxf.eztypes import Vertex, GenericLayoutType
  9. DEFAULT_ARROW_ANGLE = 18.924644
  10. DEFAULT_BETA = 45.
  11. # The base Arrow is for the 'Right' side ->| of the Dimension oriented, reverse is the 'Left' side |<-.
  12. class BaseArrow:
  13. def __init__(self, vertices: Iterable['Vertex']):
  14. self.shape = Shape2d(vertices)
  15. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  16. pass
  17. def place(self, insert: 'Vertex', angle: float):
  18. self.shape.rotate(angle)
  19. self.shape.translate(insert)
  20. class NoneStroke(BaseArrow):
  21. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  22. super().__init__([Vec2(insert)])
  23. class ObliqueStroke(BaseArrow):
  24. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  25. self.size = size
  26. s2 = size / 2
  27. # shape = [center, lower left, upper right]
  28. super().__init__([Vec2((-s2, -s2)), Vec2((s2, s2))])
  29. self.place(insert, angle)
  30. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  31. layout.add_line(start=self.shape[0], end=self.shape[1], dxfattribs=dxfattribs)
  32. class ArchTick(ObliqueStroke):
  33. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  34. width = self.size * .15
  35. if layout.dxfversion > 'AC1009':
  36. dxfattribs['const_width'] = width
  37. layout.add_lwpolyline(self.shape, format='xy', dxfattribs=dxfattribs)
  38. else:
  39. dxfattribs['default_start_width'] = width
  40. dxfattribs['default_end_width'] = width
  41. layout.add_polyline2d(self.shape, dxfattribs=dxfattribs)
  42. class ClosedArrowBlank(BaseArrow):
  43. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  44. super().__init__(open_arrow(size, angle=DEFAULT_ARROW_ANGLE))
  45. self.place(insert, angle)
  46. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  47. if layout.dxfversion > 'AC1009':
  48. polyline = layout.add_lwpolyline(
  49. points=self.shape,
  50. dxfattribs=dxfattribs)
  51. else:
  52. polyline = layout.add_polyline2d(
  53. points=self.shape,
  54. dxfattribs=dxfattribs)
  55. polyline.close(True)
  56. class ClosedArrow(ClosedArrowBlank):
  57. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  58. super().render(layout, dxfattribs)
  59. end_point = self.shape[0].lerp(self.shape[2])
  60. layout.add_line(start=self.shape[1], end=end_point, dxfattribs=dxfattribs)
  61. class ClosedArrowFilled(ClosedArrow):
  62. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  63. layout.add_solid(
  64. points=self.shape,
  65. dxfattribs=dxfattribs,
  66. )
  67. class _OpenArrow(BaseArrow):
  68. def __init__(self, arrow_angle: float, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  69. points = list(open_arrow(size, angle=arrow_angle))
  70. points.append((-1, 0))
  71. super().__init__(points)
  72. self.place(insert, angle)
  73. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  74. if layout.dxfversion > 'AC1009':
  75. layout.add_lwpolyline(points=self.shape[:-1], dxfattribs=dxfattribs)
  76. else:
  77. layout.add_polyline2d(points=self.shape[:-1], dxfattribs=dxfattribs)
  78. layout.add_line(start=self.shape[1], end=self.shape[-1], dxfattribs=dxfattribs)
  79. class OpenArrow(_OpenArrow):
  80. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  81. super().__init__(DEFAULT_ARROW_ANGLE, insert, size, angle)
  82. class OpenArrow30(_OpenArrow):
  83. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  84. super().__init__(30, insert, size, angle)
  85. class OpenArrow90(_OpenArrow):
  86. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  87. super().__init__(90, insert, size, angle)
  88. class Circle(BaseArrow):
  89. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  90. self.radius = size / 2
  91. # shape = [center point, connection point]
  92. super().__init__([
  93. Vec2((0, 0)),
  94. Vec2((-self.radius, 0)),
  95. Vec2((-size, 0)),
  96. ])
  97. self.place(insert, angle)
  98. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  99. layout.add_circle(center=self.shape[0], radius=self.radius, dxfattribs=dxfattribs)
  100. class Origin(Circle):
  101. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  102. super().render(layout, dxfattribs)
  103. layout.add_line(start=self.shape[0], end=self.shape[2], dxfattribs=dxfattribs)
  104. class CircleBlank(Circle):
  105. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  106. super().render(layout, dxfattribs)
  107. layout.add_line(start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs)
  108. class Origin2(Circle):
  109. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  110. layout.add_circle(center=self.shape[0], radius=self.radius, dxfattribs=dxfattribs)
  111. layout.add_circle(center=self.shape[0], radius=self.radius / 2, dxfattribs=dxfattribs)
  112. layout.add_line(start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs)
  113. class DotSmall(Circle):
  114. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  115. dxfattribs['closed'] = True
  116. center = self.shape[0]
  117. d = Vec2((self.radius / 2, 0))
  118. p1 = center - d
  119. p2 = center + d
  120. if layout.dxfversion > 'AC1009':
  121. dxfattribs['const_width'] = self.radius
  122. layout.add_lwpolyline([(p1, 1), (p2, 1)], format='vb', dxfattribs=dxfattribs)
  123. else:
  124. dxfattribs['default_start_width'] = self.radius
  125. dxfattribs['default_end_width'] = self.radius
  126. polyline = layout.add_polyline2d(points=[p1, p2], dxfattribs=dxfattribs)
  127. polyline[0].dxf.bulge = 1
  128. polyline[1].dxf.bulge = 1
  129. class Dot(DotSmall):
  130. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  131. layout.add_line(start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs)
  132. super().render(layout, dxfattribs)
  133. class Box(BaseArrow):
  134. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  135. # shape = [lower_left, lower_right, upper_right, upper_left, connection point]
  136. s2 = size / 2
  137. super().__init__([
  138. Vec2((-s2, -s2)),
  139. Vec2((+s2, -s2)),
  140. Vec2((+s2, +s2)),
  141. Vec2((-s2, +s2)),
  142. Vec2((-s2, 0)),
  143. Vec2((-size, 0)),
  144. ])
  145. self.place(insert, angle)
  146. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  147. if layout.dxfversion > 'AC1009':
  148. polyline = layout.add_lwpolyline(points=self.shape[0:4], dxfattribs=dxfattribs)
  149. else:
  150. polyline = layout.add_polyline2d(points=self.shape[0:4], dxfattribs=dxfattribs)
  151. polyline.close(True)
  152. layout.add_line(start=self.shape[4], end=self.shape[5], dxfattribs=dxfattribs)
  153. class BoxFilled(Box):
  154. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  155. def solid_order():
  156. v = self.shape.vertices
  157. return [v[0], v[1], v[3], v[2]]
  158. layout.add_solid(points=solid_order(), dxfattribs=dxfattribs)
  159. layout.add_line(start=self.shape[4], end=self.shape[5], dxfattribs=dxfattribs)
  160. class Integral(BaseArrow):
  161. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  162. self.radius = size * .3535534
  163. self.angle = angle
  164. # shape = [center, left_center, right_center]
  165. super().__init__([
  166. Vec2((0, 0)),
  167. Vec2((-self.radius, 0)),
  168. Vec2((self.radius, 0)),
  169. ])
  170. self.place(insert, angle)
  171. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  172. angle = self.angle
  173. layout.add_arc(center=self.shape[1], radius=self.radius, start_angle=-90 + angle, end_angle=angle,
  174. dxfattribs=dxfattribs)
  175. layout.add_arc(center=self.shape[2], radius=self.radius, start_angle=90 + angle, end_angle=180 + angle,
  176. dxfattribs=dxfattribs)
  177. class DatumTriangle(BaseArrow):
  178. REVERSE_ANGLE = 180
  179. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  180. d = .577350269 * size # tan(30)
  181. # shape = [upper_corner, lower_corner, connection_point]
  182. super().__init__([
  183. Vec2((0, d)),
  184. Vec2((0, -d)),
  185. Vec2((-size, 0)),
  186. ])
  187. self.place(insert, angle)
  188. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  189. if layout.dxfversion > 'AC1009':
  190. polyline = layout.add_lwpolyline(points=self.shape, dxfattribs=dxfattribs)
  191. else:
  192. polyline = layout.add_polyline2d(points=self.shape, dxfattribs=dxfattribs)
  193. polyline.close(True)
  194. class DatumTriangleFilled(DatumTriangle):
  195. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  196. layout.add_solid(points=self.shape, dxfattribs=dxfattribs)
  197. class _EzArrow(BaseArrow):
  198. def __init__(self, insert: 'Vertex', size: float = 1.0, angle: float = 0):
  199. points = list(arrow2(size, angle=DEFAULT_ARROW_ANGLE))
  200. points.append((-1, 0))
  201. super().__init__(points)
  202. self.place(insert, angle)
  203. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  204. if layout.dxfversion > 'AC1009':
  205. polyline = layout.add_lwpolyline(self.shape[:-1], dxfattribs=dxfattribs)
  206. else:
  207. polyline = layout.add_polyline2d(self.shape[:-1], dxfattribs=dxfattribs)
  208. polyline.close(True)
  209. class EzArrowBlank(_EzArrow):
  210. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  211. super().render(layout, dxfattribs)
  212. layout.add_line(start=self.shape[-2], end=self.shape[-1], dxfattribs=dxfattribs)
  213. class EzArrow(_EzArrow):
  214. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  215. super().render(layout, dxfattribs)
  216. layout.add_line(start=self.shape[1], end=self.shape[-1], dxfattribs=dxfattribs)
  217. class EzArrowFilled(_EzArrow):
  218. def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None):
  219. points = self.shape.vertices
  220. layout.add_solid([points[0], points[1], points[3], points[2]], dxfattribs=dxfattribs)
  221. layout.add_line(start=self.shape[-2], end=self.shape[-1], dxfattribs=dxfattribs)
  222. class _Arrows:
  223. closed_filled = ""
  224. dot = "DOT"
  225. dot_small = "DOTSMALL"
  226. dot_blank = "DOTBLANK"
  227. origin_indicator = "ORIGIN"
  228. origin_indicator_2 = "ORIGIN2"
  229. open = "OPEN"
  230. right_angle = "OPEN90"
  231. open_30 = "OPEN30"
  232. closed = "CLOSED"
  233. dot_smallblank = "SMALL"
  234. none = "NONE"
  235. oblique = "OBLIQUE"
  236. box_filled = "BOXFILLED"
  237. box = "BOXBLANK"
  238. closed_blank = "CLOSEDBLANK"
  239. datum_triangle_filled = "DATUMFILLED"
  240. datum_triangle = "DATUMBLANK"
  241. integral = "INTEGRAL"
  242. architectural_tick = "ARCHTICK"
  243. # ezdxf special arrows
  244. ez_arrow = "EZ_ARROW"
  245. ez_arrow_blank = "EZ_ARROW_BLANK"
  246. ez_arrow_filled = "EZ_ARROW_FILLED"
  247. CLASSES = {
  248. closed_filled: ClosedArrowFilled,
  249. dot: Dot,
  250. dot_small: DotSmall,
  251. dot_blank: CircleBlank,
  252. origin_indicator: Origin,
  253. origin_indicator_2: Origin2,
  254. open: OpenArrow,
  255. right_angle: OpenArrow90,
  256. open_30: OpenArrow30,
  257. closed: ClosedArrow,
  258. dot_smallblank: Circle,
  259. none: NoneStroke,
  260. oblique: ObliqueStroke,
  261. box_filled: BoxFilled,
  262. box: Box,
  263. closed_blank: ClosedArrowBlank,
  264. datum_triangle: DatumTriangle,
  265. datum_triangle_filled: DatumTriangleFilled,
  266. integral: Integral,
  267. architectural_tick: ArchTick,
  268. ez_arrow: EzArrow,
  269. ez_arrow_blank: EzArrowBlank,
  270. ez_arrow_filled: EzArrowFilled,
  271. }
  272. # arrows with origin at dimension line start/end
  273. ORIGIN_ZERO = {
  274. architectural_tick,
  275. oblique,
  276. dot_small,
  277. dot_smallblank,
  278. integral,
  279. none,
  280. }
  281. __acad__ = {
  282. closed_filled, dot, dot_small, dot_blank, origin_indicator, origin_indicator_2, open, right_angle, open_30,
  283. closed, dot_smallblank, none, oblique, box_filled, box, closed_blank, datum_triangle, datum_triangle_filled,
  284. integral, architectural_tick
  285. }
  286. __ezdxf__ = {
  287. ez_arrow,
  288. ez_arrow_blank,
  289. ez_arrow_filled,
  290. }
  291. __all_arrows__ = __acad__ | __ezdxf__
  292. EXTENSIONS_ALLOWED = {
  293. architectural_tick,
  294. oblique,
  295. none,
  296. dot_smallblank,
  297. integral,
  298. dot_small,
  299. }
  300. def is_acad_arrow(self, item: str) -> bool:
  301. return item.upper() in self.__acad__
  302. def is_ezdxf_arrow(self, item: str) -> bool:
  303. return item.upper() in self.__ezdxf__
  304. def has_extension_line(self, name):
  305. return name in self.EXTENSIONS_ALLOWED
  306. def __contains__(self, item: str) -> bool:
  307. if item is None:
  308. return False
  309. return item.upper() in self.__all_arrows__
  310. def create_block(self, blocks, name: str):
  311. block_name = self.block_name(name)
  312. if block_name not in blocks:
  313. block = blocks.new(block_name)
  314. arrow = self.arrow_shape(name, insert=(0, 0), size=1, rotation=0)
  315. arrow.render(block, dxfattribs={'color': 0, 'linetype': 'BYBLOCK'})
  316. return block_name
  317. def block_name(self, name):
  318. if not self.is_acad_arrow(name): # common BLOCK definition
  319. return name.upper() # e.g. Dimension.dxf.bkl = 'EZ_ARROW' == Insert.dxf.name
  320. elif name == "": # special AutoCAD arrow symbols 'CLOSED_FILLED' has no name
  321. # ezdxf uses blocks for ALL arrows, but '_' (closed filled) as block name?
  322. return "_CLOSEDFILLED" # Dimension.dxf.bkl = '' != Insert.dxf.name = '_CLOSED_FILLED'
  323. else: # special AutoCAD arrow symbols have leading '_' as common practice!
  324. return '_' + name.upper() # Dimension.dxf.bkl = 'DOT' != Insert.dxf.name = '_DOT'
  325. def arrow_name(self, block_name: str) -> str:
  326. if block_name.startswith('_'):
  327. name = block_name[1:].upper()
  328. if name == 'CLOSEDFILLED':
  329. return ''
  330. elif self.is_acad_arrow(name):
  331. return name
  332. return block_name
  333. def insert_arrow(self, layout: 'GenericLayoutType',
  334. name: str,
  335. insert: 'Vertex',
  336. size: float = 1.,
  337. rotation: float = 0,
  338. dxfattribs: dict = None) -> Vec2:
  339. block_name = self.create_block(layout.drawing.blocks, name)
  340. dxfattribs = dict(dxfattribs) if dxfattribs else {} # copy attribs
  341. dxfattribs['rotation'] = rotation
  342. dxfattribs['xscale'] = size
  343. dxfattribs['yscale'] = size
  344. layout.add_blockref(block_name, insert=insert, dxfattribs=dxfattribs)
  345. return connection_point(name, insert=insert, scale=size, rotation=rotation)
  346. def render_arrow(self, layout: 'GenericLayoutType',
  347. name: str,
  348. insert: 'Vertex',
  349. size: float = 1.,
  350. rotation: float = 0,
  351. dxfattribs: dict = None) -> Vec2:
  352. dxfattribs = dxfattribs or {}
  353. arrow = self.arrow_shape(name, insert, size, rotation)
  354. arrow.render(layout, dxfattribs)
  355. return connection_point(name, insert=insert, scale=size, rotation=rotation)
  356. def arrow_shape(self, name: str, insert: 'Vertex', size: float, rotation: float) -> BaseArrow:
  357. # size depending shapes
  358. if name == self.dot_small:
  359. size *= .25
  360. elif name == self.dot_smallblank:
  361. size *= .5
  362. cls = self.CLASSES[name]
  363. return cls(insert, size, rotation)
  364. def connection_point(arrow_name: str, insert: 'Vertex', scale: float = 1, rotation: float = 0) -> Vec2:
  365. insert = Vec2(insert)
  366. if arrow_name in _Arrows.ORIGIN_ZERO:
  367. return insert
  368. else:
  369. return insert - Vec2.from_deg_angle(rotation, scale)
  370. ARROWS = _Arrows()