dimstyleoverride.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. from typing import Any, TYPE_CHECKING, Tuple
  2. from ezdxf.lldxf import const
  3. from ezdxf.lldxf.const import DXFAttributeError, DIMJUST, DIMTAD
  4. from ezdxf.render.arrows import ARROWS
  5. from ezdxf.math import Vector
  6. import logging
  7. logger = logging.getLogger('ezdxf')
  8. if TYPE_CHECKING:
  9. from ezdxf.eztypes import Dimension, UCS, Drawing, DimStyle, Vertex, BaseDimensionRenderer
  10. class DimStyleOverride:
  11. def __init__(self, dimension: 'Dimension', override: dict = None):
  12. self.dimension = dimension # type: Dimension
  13. dim_style_name = dimension.get_dxf_attrib('dimstyle', 'STANDARD')
  14. self.dimstyle = self.drawing.dimstyles.get(dim_style_name) # type: DimStyle
  15. self.dimstyle_attribs = self.get_dstyle_dict() # type: dict
  16. # special ezdxf attributes beyond the DXF reference, therefore not stored in the DSTYLE data.
  17. # This are only rendering effects or data transfer objects
  18. # user_location: Vector - user location override if not None
  19. # relative_user_location: bool - user location override relative to dimline center if True
  20. # text_shift_h: float - shift text in text direction, relative to standard text location
  21. # text_shift_v: float - shift text perpendicular to text direction, relative to standard text location
  22. self.update(override or {})
  23. @property
  24. def drawing(self) -> 'Drawing':
  25. return self.dimension.drawing
  26. @property
  27. def dxfversion(self) -> str:
  28. return self.dimension.drawing.dxfversion
  29. def get_dstyle_dict(self) -> dict:
  30. return self.dimension.get_acad_dstyle(self.dimstyle)
  31. def get(self, attribute: str, default: Any = None) -> Any:
  32. if attribute in self.dimstyle_attribs:
  33. result = self.dimstyle_attribs[attribute]
  34. else:
  35. # Return default value for attributes not supported by DXF R12.
  36. # This is a hack to use the same algorithm to render DXF R2000 and DXF R12 DIMENSION entities.
  37. # But the DXF R2000 attributes are not stored in the DXF R12 file!!!
  38. # Does not catch invalid attributes names! Look into debug log for ignored DIMSTYLE attributes.
  39. try:
  40. result = self.dimstyle.get_dxf_attrib(attribute, default)
  41. except DXFAttributeError:
  42. # return default value
  43. result = default
  44. return result
  45. def pop(self, attribute: str, default: Any = None) -> Any:
  46. value = self.get(attribute, default)
  47. # delete just from override dict
  48. del self[attribute]
  49. return value
  50. def update(self, attribs: dict) -> None:
  51. self.dimstyle_attribs.update(attribs)
  52. def __getitem__(self, item: str) -> Any:
  53. return self.get(item)
  54. def __setitem__(self, key: str, value: Any) -> None:
  55. self.dimstyle_attribs[key] = value
  56. def __delitem__(self, key: str) -> None:
  57. try:
  58. del self.dimstyle_attribs[key]
  59. except KeyError: # silent discard
  60. pass
  61. def commit(self) -> None:
  62. """
  63. Write overwritten DIMSTYLE attributes into XDATA section of the DIMENSION entity.
  64. """
  65. def set_arrow_handle(attrib_name, block_name):
  66. attrib_name += '_handle'
  67. if block_name in ARROWS: # create all arrows on demand
  68. block_name = ARROWS.create_block(blocks, block_name)
  69. if block_name == '_CLOSEDFILLED': # special arrow
  70. handle = '0' # set special #0 handle for closed filled arrow
  71. else:
  72. block = blocks.get(block_name)
  73. handle = block.block_record_handle
  74. self.dimstyle_attribs[attrib_name] = handle
  75. def set_linetype_handle(attrib_name, linetype_name):
  76. ltype = self.drawing.linetypes.get(linetype_name)
  77. self.dimstyle_attribs[attrib_name + '_handle'] = ltype.dxf.handle
  78. if self.drawing.dxfversion > 'AC1009':
  79. # transform block names into block record handles
  80. blocks = self.drawing.blocks
  81. for attrib_name in ('dimblk', 'dimblk1', 'dimblk2', 'dimldrblk'):
  82. try:
  83. block_name = self.dimstyle_attribs.pop(attrib_name)
  84. except KeyError:
  85. pass
  86. else:
  87. set_arrow_handle(attrib_name, block_name)
  88. if self.drawing.dxfversion >= 'AC1021':
  89. # transform linetype names into LTYPE entry handles
  90. for attrib_name in ('dimltype', 'dimltex1', 'dimltex2'):
  91. try:
  92. linetype_name = self.dimstyle_attribs.pop(attrib_name)
  93. except KeyError:
  94. pass
  95. else:
  96. set_linetype_handle(attrib_name, linetype_name)
  97. self.dimension.set_acad_dstyle(self.dimstyle_attribs)
  98. def set_arrows(self, blk: str = None, blk1: str = None, blk2: str = None, ldrblk: str = None,
  99. size: float = None) -> None:
  100. """
  101. Set arrows or user defined blocks and disable oblique stroke as tick.
  102. Args:
  103. blk: defines both arrows at once as name str or user defined block (name)
  104. blk1: defines left arrow as name str or as user defined block (name)
  105. blk2: defines right arrow as name str or as user defined block (name)
  106. ldrblk: defines leader arrow as name str or as user defined block (name)
  107. size: arrow size in drawing units
  108. """
  109. def set_arrow(dimvar: str, name: str) -> None:
  110. self.dimstyle_attribs[dimvar] = name
  111. if size is not None:
  112. self.dimstyle_attribs['dimasz'] = float(size)
  113. if blk is not None:
  114. set_arrow('dimblk', blk)
  115. self.dimstyle_attribs['dimsah'] = 0
  116. self.dimstyle_attribs['dimtsz'] = 0. # use arrows
  117. if blk1 is not None:
  118. set_arrow('dimblk1', blk1)
  119. self.dimstyle_attribs['dimsah'] = 1
  120. self.dimstyle_attribs['dimtsz'] = 0. # use arrows
  121. if blk2 is not None:
  122. set_arrow('dimblk2', blk2)
  123. self.dimstyle_attribs['dimsah'] = 1
  124. self.dimstyle_attribs['dimtsz'] = 0. # use arrows
  125. if ldrblk is not None:
  126. set_arrow('dimldrblk', ldrblk)
  127. def get_arrow_names(self) -> Tuple[str, str]:
  128. """
  129. Get arrows as name strings like 'ARCHTICK'.
  130. """
  131. dimtsz = self.get('dimtsz')
  132. blk1, blk2 = None, None
  133. if dimtsz == 0.:
  134. if bool(self.get('dimsah')):
  135. blk1 = self.get('dimblk1')
  136. blk2 = self.get('dimblk2')
  137. else:
  138. blk = self.get('dimblk')
  139. blk1 = blk
  140. blk2 = blk
  141. return blk1, blk2
  142. def set_tick(self, size: float = 1) -> None:
  143. """
  144. Use oblique stroke as tick, disables arrows.
  145. Args:
  146. size: arrow size in daring units
  147. """
  148. self.dimstyle_attribs['dimtsz'] = float(size)
  149. def set_text_align(self, halign: str = None, valign: str = None, vshift: float = None) -> None:
  150. """
  151. Set measurement text alignment, `halign` defines the horizontal alignment, `valign` defines the vertical
  152. alignment, `above1` and `above2` means above extension line 1 or 2 and aligned with extension line.
  153. Args:
  154. halign: `left`, `right`, `center`, `above1`, `above2`, requires DXF R2000+
  155. valign: `above`, `center`, `below`
  156. vshift: vertical text shift, if `valign` is `center`; >0 shift upward, <0 shift downwards
  157. """
  158. if halign:
  159. self.dimstyle_attribs['dimjust'] = DIMJUST[halign.lower()]
  160. if valign:
  161. valign = valign.lower()
  162. self.dimstyle_attribs['dimtad'] = DIMTAD[valign]
  163. if valign == 'center' and vshift is not None:
  164. self.dimstyle_attribs['dimtvp'] = float(vshift)
  165. def set_tolerance(self, upper: float, lower: float = None, hfactor: float = None,
  166. align: str = None, dec: int = None, leading_zeros: bool = None,
  167. trailing_zeros: bool = None) -> None:
  168. """
  169. Set tolerance text format, upper and lower value, text height factor, number of decimal places or leading and
  170. trailing zero suppression.
  171. Args:
  172. upper: upper tolerance value
  173. lower: lower tolerance value, if None same as upper
  174. hfactor: tolerance text height factor in relation to the dimension text height
  175. align: tolerance text alignment "TOP", "MIDDLE", "BOTTOM"
  176. dec: Sets the number of decimal places displayed
  177. leading_zeros: suppress leading zeros for decimal dimensions if False
  178. trailing_zeros: suppress trailing zeros for decimal dimensions if False
  179. """
  180. self.dimstyle_attribs['dimtol'] = 1
  181. self.dimstyle_attribs['dimlim'] = 0
  182. self.dimstyle_attribs['dimtp'] = float(upper)
  183. if lower is not None:
  184. self.dimstyle_attribs['dimtm'] = float(lower)
  185. else:
  186. self.dimstyle_attribs['dimtm'] = float(upper)
  187. if hfactor is not None:
  188. self.dimstyle_attribs['dimtfac'] = float(hfactor)
  189. if align is not None:
  190. self.dimstyle_attribs['dimtolj'] = const.MTEXT_INLINE_ALIGN[align.upper()]
  191. if dec is not None:
  192. self.dimstyle_attribs['dimtdec'] = dec
  193. # works only with decimal dimensions not inch and feet, US user set dimzin directly
  194. if leading_zeros is not None or trailing_zeros is not None:
  195. dimtzin = 0
  196. if leading_zeros is False:
  197. dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
  198. if trailing_zeros is False:
  199. dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
  200. self.dimstyle_attribs['dimtzin'] = dimtzin
  201. def set_limits(self, upper: float, lower: float, hfactor: float = None,
  202. dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None:
  203. """
  204. Set limits text format, upper and lower limit values, text height factor, number of decimal places or
  205. leading and trailing zero suppression.
  206. Args:
  207. upper: upper limit value added to measurement value
  208. lower: lower lower value subtracted from measurement value
  209. hfactor: limit text height factor in relation to the dimension text height
  210. dec: Sets the number of decimal places displayed, required DXF R2000+
  211. leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+
  212. trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+
  213. """
  214. # exclusive limits
  215. self.dimstyle_attribs['dimlim'] = 1
  216. self.dimstyle_attribs['dimtol'] = 0
  217. self.dimstyle_attribs['dimtp'] = float(upper)
  218. self.dimstyle_attribs['dimtm'] = float(lower)
  219. if hfactor is not None:
  220. self.dimstyle_attribs['dimtfac'] = float(hfactor)
  221. # works only with decimal dimensions not inch and feet, US user set dimzin directly
  222. if leading_zeros is not None or trailing_zeros is not None:
  223. dimtzin = 0
  224. if leading_zeros is False:
  225. dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
  226. if trailing_zeros is False:
  227. dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
  228. self.dimstyle_attribs['dimtzin'] = dimtzin
  229. if dec is not None:
  230. self.dimstyle_attribs['dimtdec'] = int(dec)
  231. def set_text_format(self, prefix: str = '', postfix: str = '', rnd: float = None, dec: int = None, sep: str = None,
  232. leading_zeros: bool = None, trailing_zeros: bool = None) -> None:
  233. """
  234. Set dimension text format, like prefix and postfix string, rounding rule and number of decimal places.
  235. Args:
  236. prefix: dimension text prefix text as string
  237. postfix: dimension text postfix text as string
  238. rnd: Rounds all dimensioning distances to the specified value, for instance, if DIMRND is set to 0.25, all
  239. distances round to the nearest 0.25 unit. If you set DIMRND to 1.0, all distances round to the nearest
  240. integer.
  241. dec: Sets the number of decimal places displayed for the primary units of a dimension. requires DXF R2000+
  242. sep: "." or "," as decimal separator
  243. leading_zeros: suppress leading zeros for decimal dimensions if False
  244. trailing_zeros: suppress trailing zeros for decimal dimensions if False
  245. """
  246. if prefix or postfix:
  247. self.dimstyle_attribs['dimpost'] = prefix + '<>' + postfix
  248. if rnd is not None:
  249. self.dimstyle_attribs['dimrnd'] = rnd
  250. if dec is not None:
  251. self.dimstyle_attribs['dimdec'] = dec
  252. if sep is not None:
  253. self.dimstyle_attribs['dimdsep'] = ord(sep)
  254. # works only with decimal dimensions not inch and feet, US user set dimzin directly
  255. if leading_zeros is not None or trailing_zeros is not None:
  256. dimzin = 0
  257. if leading_zeros is False:
  258. dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
  259. if trailing_zeros is False:
  260. dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
  261. self.dimstyle_attribs['dimzin'] = dimzin
  262. def set_dimline_format(self, color: int = None, linetype: str = None, lineweight: int = None,
  263. extension: float = None, disable1: bool = None, disable2: bool = None):
  264. """
  265. Set dimension line properties
  266. Args:
  267. color: color index
  268. linetype: linetype as string
  269. lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
  270. extension: extension length
  271. disable1: True to suppress first part of dimension line
  272. disable2: True to suppress second part of dimension line
  273. """
  274. if color is not None:
  275. self.dimstyle_attribs['dimclrd'] = color
  276. if linetype is not None:
  277. self.dimstyle_attribs['dimltype'] = linetype
  278. if lineweight is not None:
  279. self.dimstyle_attribs['dimlwd'] = lineweight
  280. if extension is not None:
  281. self.dimstyle_attribs['dimdle'] = extension
  282. if disable1 is not None:
  283. self.dimstyle_attribs['dimsd1'] = disable1
  284. if disable2 is not None:
  285. self.dimstyle_attribs['dimsd2'] = disable2
  286. def set_extline_format(self, color: int = None, lineweight: int = None, extension: float = None,
  287. offset: float = None, fixed_length: float = None):
  288. """
  289. Set common extension line attributes.
  290. Args:
  291. color: color index
  292. lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
  293. extension: extension length above dimension line
  294. offset: offset from measurement point
  295. fixed_length: set fixed length extension line, length below the dimension line
  296. """
  297. if color is not None:
  298. self.dimstyle_attribs['dimclre'] = color
  299. if lineweight is not None:
  300. self.dimstyle_attribs['dimlwe'] = lineweight
  301. if extension is not None:
  302. self.dimstyle_attribs['dimexe'] = extension
  303. if offset is not None:
  304. self.dimstyle_attribs['dimexo'] = offset
  305. if fixed_length is not None:
  306. self.dimstyle_attribs['dimflxon'] = 1
  307. self.dimstyle_attribs['dimflx'] = fixed_length
  308. def set_extline1(self, linetype: str = None, disable=False):
  309. """
  310. Set extension line 1 attributes.
  311. Args:
  312. linetype: linetype for extension line 1
  313. disable: disable extension line 1 if True
  314. """
  315. if linetype is not None:
  316. self.dimstyle_attribs['dimltex1'] = linetype
  317. if disable:
  318. self.dimstyle_attribs['dimse1'] = 1
  319. def set_extline2(self, linetype: str = None, disable=False):
  320. """
  321. Set extension line 2 attributes.
  322. Args:
  323. linetype: linetype for extension line 2
  324. disable: disable extension line 2 if True
  325. """
  326. if linetype is not None:
  327. self.dimstyle_attribs['dimltex2'] = linetype
  328. if disable:
  329. self.dimstyle_attribs['dimse2'] = 1
  330. def set_text(self, text='<>') -> None:
  331. """
  332. Set dimension text.
  333. - text == ' ' ... suppress dimension text
  334. - text == '' or '<>' ... use measured distance as dimension text
  335. - else use text literally
  336. Args:
  337. text: string
  338. """
  339. self.dimension.dxf.text = text
  340. def shift_text(self, dh: float, dv: float) -> None:
  341. """
  342. Set relative text movement, implemented as user location override without leader.
  343. Args:
  344. dh: shift text in text direction
  345. dv: shift text perpendicular to text direction
  346. """
  347. self.dimstyle_attribs['text_shift_h'] = dh
  348. self.dimstyle_attribs['text_shift_v'] = dv
  349. def set_location(self, location: 'Vertex', leader=False, relative=False):
  350. self.dimstyle_attribs['dimtmove'] = 1 if leader else 2
  351. self.dimension.set_flag_state(self.dimension.USER_LOCATION_OVERRIDE, state=True, name='dimtype')
  352. self.dimstyle_attribs['user_location'] = Vector(location)
  353. self.dimstyle_attribs['relative_user_location'] = relative
  354. def get_renderer(self, ucs: 'UCS' = None):
  355. return self.drawing.dimension_renderer.dispatch(self, ucs)
  356. def render(self, ucs: 'UCS' = None, discard=False) -> 'BaseDimensionRenderer':
  357. """
  358. Initiate dimension line rendering process and also writes overridden dimension style attributes into the DSTYLE
  359. XDATA section.
  360. For a friendly CAD applications like BricsCAD you can discard the dimension line rendering, because it is done
  361. automatically by BricsCAD, if no dimension rendering BLOCK is available and it is likely to get better results
  362. as by ezdxf.
  363. AutoCAD does not render DIMENSION entities automatically, so I rate AutoCAD as unfriendly CAD application.
  364. Args:
  365. ucs: user coordinate system
  366. discard: discard rendering done by ezdxf (works with BricsCAD, but not with AutoCAD)
  367. Returns: used renderer for analytics
  368. """
  369. renderer = self.get_renderer(ucs)
  370. if discard:
  371. self.drawing.add_acad_incompatibility_message('DIMENSION without geometry as BLOCK (discard=True)')
  372. else:
  373. block = self.drawing.blocks.new_anonymous_block(type_char='D')
  374. self.dimension.dxf.geometry = block.name
  375. renderer.render(block)
  376. # should be called after rendering
  377. renderer.finalize()
  378. if len(self.dimstyle_attribs):
  379. self.commit()
  380. return renderer