dimension.py 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340
  1. # Created: 28.12.2018
  2. # Copyright (C) 2018-2019, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, Tuple, Iterable, List, cast, Optional
  5. import math
  6. from ezdxf.math import Vector, Vec2, ConstructionRay, xround, ConstructionLine, ConstructionBox
  7. from ezdxf.math import UCS, PassTroughUCS
  8. from ezdxf.lldxf import const
  9. from ezdxf.options import options
  10. from ezdxf.lldxf.const import DXFValueError, DXFUndefinedBlockError
  11. from ezdxf.tools import suppress_zeros
  12. from ezdxf.render.arrows import ARROWS, connection_point
  13. from ezdxf.dimstyleoverride import DimStyleOverride
  14. if TYPE_CHECKING:
  15. from ezdxf.eztypes import Dimension, Vertex, Drawing, GenericLayoutType, Style
  16. class TextBox(ConstructionBox):
  17. """
  18. Text boundaries representation.
  19. """
  20. def __init__(self, center: 'Vertex', width: float, height: float, angle: float, gap: float = 0):
  21. height += (2 * gap)
  22. super().__init__(center, width, height, angle)
  23. PLUS_MINUS = '±'
  24. _TOLERANCE_COMMON = r"\A{align};{txt}{{\H{fac:.2f}x;"
  25. TOLERANCE_TEMPLATE1 = _TOLERANCE_COMMON + r"{tol}}}"
  26. TOLERANCE_TEMPLATE2 = _TOLERANCE_COMMON + r"\S{upr}^ {lwr};}}"
  27. LIMITS_TEMPLATE = r"{{\H{fac:.2f}x;\S{upr}^ {lwr};}}"
  28. def OptionalVec2(v) -> Optional[Vec2]:
  29. if v is not None:
  30. return Vec2(v)
  31. else:
  32. return None
  33. class BaseDimensionRenderer:
  34. """
  35. Base rendering class for DIMENSION entities.
  36. """
  37. def __init__(self, dimension: 'Dimension', ucs: 'UCS' = None, override: DimStyleOverride = None):
  38. # DXF document
  39. self.drawing = dimension.drawing # type: Drawing
  40. # DIMENSION entity
  41. self.dimension = dimension # type: Dimension
  42. self.dxfversion = self.drawing.dxfversion # type: str
  43. self.supports_dxf_r2000 = self.dxfversion >= 'AC1015' # type: bool
  44. self.supports_dxf_r2007 = self.dxfversion >= 'AC1021' # type: bool
  45. # Target BLOCK of the graphical representation of the DIMENSION entity
  46. self.block = None # type: GenericLayoutType
  47. # DimStyleOverride object, manages dimension style overriding
  48. if override:
  49. self.dim_style = override
  50. else:
  51. self.dim_style = DimStyleOverride(dimension)
  52. # User defined coordinate system for DIMENSION entity
  53. self.ucs = ucs or PassTroughUCS()
  54. self.requires_extrusion = self.ucs.uz != (0, 0, 1) # type: bool
  55. # ezdxf specific attributes beyond DXF reference, therefore not stored in the DXF file (DSTYLE)
  56. # Some of these are just an rendering effect, which will be ignored by CAD applications if they modify the
  57. # DIMENSION entity
  58. # user location override as UCS coordinates, stored as text_midpoint in the DIMENSION entity
  59. self.user_location = OptionalVec2(self.dim_style.pop('user_location', None))
  60. # user location override relative to dimline center if True
  61. self.relative_user_location = self.dim_style.pop('relative_user_location', False) # type: bool
  62. # shift text away from default text location - implemented as user location override without leader
  63. # shift text along in text direction
  64. self.text_shift_h = self.dim_style.pop('text_shift_h', 0.) # type: float
  65. # shift text perpendicular to text direction
  66. self.text_shift_v = self.dim_style.pop('text_shift_v', 0.) # type: float
  67. # suppress arrow rendering - only rendering is suppressed (rendering effect), all placing related calculations
  68. # are done without this settings. Used for multi point linear dimensions to avoid double rendering of non arrow
  69. # ticks.
  70. self.suppress_arrow1 = self.dim_style.pop('suppress_arrow1', False) # type: bool
  71. self.suppress_arrow2 = self.dim_style.pop('suppress_arrow2', False) # type: bool
  72. # end of ezdxf specific attributes
  73. # ---------------------------------------------
  74. # GENERAL PROPERTIES
  75. # ---------------------------------------------
  76. self.default_color = self.dimension.dxf.color # type: int
  77. self.default_layer = self.dimension.dxf.layer # type: str
  78. # ezdxf creates ALWAYS attachment points in the text center.
  79. self.text_attachment_point = 5 # type: int # fixed for ezdxf rendering
  80. # ignored by ezdxf
  81. self.horizontal_direction = self.dimension.get_dxf_attrib('horizontal_direction', None) # type: bool
  82. get = self.dim_style.get
  83. # overall scaling of DIMENSION entity
  84. self.dim_scale = get('dimscale', 1) # type: float
  85. if self.dim_scale == 0:
  86. self.dim_scale = 1
  87. # Controls drawing of circle or arc center marks and centerlines, for DIMDIAMETER and DIMRADIUS, the center
  88. # mark is drawn only if you place the dimension line outside the circle or arc.
  89. # 0 = No center marks or lines are drawn
  90. # <0 = Center lines are drawn
  91. # >0 = Center marks are drawn
  92. self.dim_center_marks = get('dimcen', 0) # type: int # not supported yet
  93. # ---------------------------------------------
  94. # TEXT
  95. # ---------------------------------------------
  96. # dimension measurement factor
  97. self.dim_measurement_factor = get('dimlfac', 1) # type: float
  98. self.text_style_name = get('dimtxsty', options.default_dimension_text_style) # type: str
  99. self.text_style = self.drawing.styles.get(self.text_style_name) # type: Style
  100. self.text_height = self.char_height * self.dim_scale # type: float
  101. self.text_width_factor = self.text_style.get_dxf_attrib('width', 1.) # type: float
  102. # text_gap: gap between dimension line an dimension text
  103. self.text_gap = get('dimgap', 0.625) * self.dim_scale # type: float
  104. # user defined text rotation - overrides everything
  105. self.user_text_rotation = self.dimension.get_dxf_attrib('text_rotation', None) # type: float
  106. # calculated text rotation
  107. self.text_rotation = self.user_text_rotation # type: float
  108. self.text_color = get('dimclrt', self.default_color) # type: int
  109. self.text_round = get('dimrnd', None) # type: float
  110. self.text_decimal_places = get('dimdec', None) # type: int
  111. # Controls the suppression of zeros in the primary unit value.
  112. # Values 0-3 affect feet-and-inch dimensions only and are not supported
  113. # 4 (Bit 3) = Suppresses leading zeros in decimal dimensions (for example, 0.5000 becomes .5000)
  114. # 8 (Bit 4) = Suppresses trailing zeros in decimal dimensions (for example, 12.5000 becomes 12.5)
  115. # 12 (Bit 3+4) = Suppresses both leading and trailing zeros (for example, 0.5000 becomes .5)
  116. self.text_suppress_zeros = get('dimzin', 0) # type: int
  117. dimdsep = self.dim_style.get('dimdsep', 0)
  118. self.text_decimal_separator = ',' if dimdsep == 0 else chr(dimdsep) # type: str
  119. self.text_format = self.dim_style.get('dimpost', '<>') # type: str
  120. self.text_fill = self.dim_style.get('dimtfill', 0) # type: int # 0= None, 1=Background, 2=DIMTFILLCLR
  121. self.text_fill_color = self.dim_style.get('dimtfillclr', 1) # type: int
  122. self.text_box_fill_scale = 1.1
  123. # text_halign = 0: center; 1: left; 2: right; 3: above ext1; 4: above ext2
  124. self.text_halign = get('dimjust', 0) # type: int
  125. # text_valign = 0: center; 1: above; 2: farthest away?; 3: JIS?; 4: below (2, 3 ignored by ezdxf)
  126. self.text_valign = get('dimtad', 0) # type: int
  127. # Controls the vertical position of dimension text above or below the dimension line, when DIMTAD = 0.
  128. # The magnitude of the vertical offset of text is the product of the text height (+gap?) and DIMTVP.
  129. # Setting DIMTVP to 1.0 is equivalent to setting DIMTAD = 1.
  130. self.text_vertical_position = get('dimtvp', 0.) # type: float # not supported yet
  131. self.text_movement_rule = get('dimtmove', 2) # type: int # move text freely
  132. if self.text_movement_rule == 0:
  133. # moves the dimension line with dimension text and makes no sense for ezdxf (just set `base` argument)
  134. self.text_movement_rule = 2
  135. # requires a leader?
  136. self.text_has_leader = self.user_location is not None and self.text_movement_rule == 1 # type: bool
  137. # text_rotation=0 if dimension text is 'inside', ezdxf defines 'inside' as at the default text location
  138. self.text_inside_horizontal = get('dimtih', 0) # type: bool
  139. # text_rotation=0 if dimension text is 'outside', ezdxf defines 'outside' as NOT at the default text location
  140. self.text_outside_horizontal = get('dimtoh', 0) # type: bool
  141. # force text location 'inside', even if the text should be moved 'outside'
  142. self.force_text_inside = bool(get('dimtix', 0)) # type: bool
  143. # how dimension text and arrows are arranged when space is not sufficient to place both 'inside'
  144. # 0 = Places both text and arrows outside extension lines
  145. # 1 = Moves arrows first, then text
  146. # 2 = Moves text first, then arrows
  147. # 3 = Moves either text or arrows, whichever fits best
  148. self.text_fitting_rule = get('dimatfit', 2) # type: int # not supported yet - ezdxf behaves like 2
  149. # units for all dimension types except Angular.
  150. # 1 = Scientific
  151. # 2 = Decimal
  152. # 3 = Engineering
  153. # 4 = Architectural (always displayed stacked)
  154. # 5 = Fractional (always displayed stacked)
  155. self.text_length_unit = get('dimlunit', 2) # type: int # not supported yet - ezdxf behaves like 2
  156. # fraction format when DIMLUNIT is set to 4 (Architectural) or 5 (Fractional).
  157. # 0 = Horizontal stacking
  158. # 1 = Diagonal stacking
  159. # 2 = Not stacked (for example, 1/2)
  160. self.text_fraction_format = get('dimfrac', 0) # type: int # not supported
  161. # units format for angular dimensions
  162. # 0 = Decimal degrees
  163. # 1 = Degrees/minutes/seconds (not supported) same as 0
  164. # 2 = Grad
  165. # 3 = Radians
  166. self.text_angle_unit = get('dimaunit', 0) # type: int
  167. # text_outside is only True if really placed outside of default text location
  168. # remark: user defined text location is always outside per definition (not by real location)
  169. self.text_outside = False
  170. # calculated or overridden dimension text location
  171. self.text_location = None # type: Vec2
  172. # bounding box of dimension text including border space
  173. self.text_box = None # type: TextBox
  174. # formatted dimension text
  175. self.text = ""
  176. # True if dimension text doesn't fit between extension lines
  177. self.is_wide_text = False
  178. # ---------------------------------------------
  179. # ARROWS & TICKS
  180. # ---------------------------------------------
  181. self.tick_size = get('dimtsz') * self.dim_scale
  182. if self.tick_size > 0:
  183. # use oblique strokes as 'arrows', disables usual 'arrows' and user defined blocks
  184. self.arrow1_name, self.arrow2_name = None, None # type: str
  185. # tick size is per definition double the size of arrow size
  186. # adjust arrow size to reuse the 'oblique' arrow block
  187. self.arrow_size = self.tick_size * 2 # type: float
  188. else:
  189. # arrow name or block name if user defined arrow
  190. self.arrow1_name, self.arrow2_name = self.dim_style.get_arrow_names() # type: str
  191. self.arrow_size = get('dimasz') * self.dim_scale # type: float
  192. # Suppresses arrowheads if not enough space is available inside the extension lines.
  193. # Only if force_text_inside is True
  194. self.suppress_arrow_heads = get('dimsoxd', 0) # type: bool # not supported yet
  195. # ---------------------------------------------
  196. # DIMENSION LINE
  197. # ---------------------------------------------
  198. self.dim_line_color = get('dimclrd', self.default_color) # type: int
  199. # dimension line extension, along the dimension line direction ('left' and 'right')
  200. self.dim_line_extension = get('dimdle', 0.) * self.dim_scale # type: float
  201. self.dim_linetype = get('dimltype', None) # type: str
  202. self.dim_lineweight = get('dimlwd', const.LINEWEIGHT_BYBLOCK) # type: int
  203. # suppress first part of the dimension line
  204. self.suppress_dim1_line = get('dimsd1', 0) # type: bool
  205. # suppress second part of the dimension line
  206. self.suppress_dim2_line = get('dimsd2', 0) # type: bool
  207. # Controls whether a dimension line is drawn between the extension lines even when the text is placed outside.
  208. # For radius and diameter dimensions (when DIMTIX is off), draws a dimension line inside the circle or arc and
  209. # places the text, arrowheads, and leader outside.
  210. # 0 = no dimension line
  211. # 1 = draw dimension line
  212. self.dim_line_if_text_outside = get('dimtofl', 1) # type: int # not supported yet - ezdxf behaves like 1
  213. # ---------------------------------------------
  214. # EXTENSION LINES
  215. # ---------------------------------------------
  216. self.ext_line_color = get('dimclre', self.default_color)
  217. self.ext1_linetype_name = get('dimltex1', None) # type: str
  218. self.ext2_linetype_name = get('dimltex2', None) # type: str
  219. self.ext_lineweight = get('dimlwe', const.LINEWEIGHT_BYBLOCK)
  220. self.suppress_ext1_line = get('dimse1', 0) # type: bool
  221. self.suppress_ext2_line = get('dimse2', 0) # type: bool
  222. # extension of extension line above the dimension line, in extension line direction
  223. # in most cases perpendicular to dimension line (oblique!)
  224. self.ext_line_extension = get('dimexe', 0.) * self.dim_scale # type: float
  225. # distance of extension line from the measurement point in extension line direction
  226. self.ext_line_offset = get('dimexo', 0.) * self.dim_scale # type: float
  227. # fixed length extension line, leenght above dimension line is still self.ext_line_extension
  228. self.ext_line_fixed = get('dimflxon', 0) # type: bool
  229. # length below the dimension line:
  230. self.ext_line_length = get('dimflx', self.ext_line_extension) * self.dim_scale # type: float
  231. # ---------------------------------------------
  232. # TOLERANCES & LIMITS
  233. # ---------------------------------------------
  234. # appends tolerances to dimension text. Setting DIMTOL to on turns DIMLIM off.
  235. self.dim_tolerance = get('dimtol', 0) # type: bool
  236. # generates dimension limits as the default text. Setting DIMLIM to On turns DIMTOL off.
  237. self.dim_limits = get('dimlim', 0) # type: bool
  238. if self.dim_tolerance:
  239. self.dim_limits = 0
  240. if self.dim_limits:
  241. self.dim_tolerance = 0
  242. # scale factor for the text height of fractions and tolerance values relative to the dimension text height
  243. self.tol_text_scale_factor = get('dimtfac', .5)
  244. self.tol_line_spacing = 1.35 # default MTEXT line spacing for tolerances (BricsCAD)
  245. # sets the minimum (or lower) tolerance limit for dimension text when DIMTOL or DIMLIM is on.
  246. # DIMTM accepts signed values. If DIMTOL is on and DIMTP and DIMTM are set to the same value, a tolerance value
  247. # is drawn. If DIMTM and DIMTP values differ, the upper tolerance is drawn above the lower, and a plus sign is
  248. # added to the DIMTP value if it is positive. For DIMTM, the program uses the negative of the value you enter
  249. # (adding a minus sign if you specify a positive number and a plus sign if you specify a negative number).
  250. self.tol_minimum = get('dimtm', 0) # type: float
  251. # Sets the maximum (or upper) tolerance limit for dimension text when DIMTOL or DIMLIM is on. DIMTP accepts
  252. # signed values. If DIMTOL is on and DIMTP and DIMTM are set to the same value, a tolerance value is drawn.
  253. # If DIMTM and DIMTP values differ, the upper tolerance is drawn above the lower and a plus sign is added to
  254. # the DIMTP value if it is positive.
  255. self.tol_maximum = get('dimtp', 0) # type: float
  256. # number of decimal places to display in tolerance values
  257. self.tol_decimal_places = get('dimtdec', 4) # type: int
  258. # vertical justification for tolerance values relative to the nominal dimension text
  259. # 0 = Bottom
  260. # 1 = Middle
  261. # 2 = Top
  262. self.tol_valign = get('dimtolj', 0) # type: int
  263. # same as DIMZIN for tolerances (self.text_suppress_zeros)
  264. self.tol_suppress_zeros = get('dimtzin', 0) # type: int
  265. self.tol_text = None
  266. self.tol_text_height = 0.
  267. self.tol_text_upper = None
  268. self.tol_text_lower = None
  269. self.tol_char_height = self.char_height * self.tol_text_scale_factor * self.dim_scale
  270. # tolerances
  271. if self.dim_tolerance:
  272. # single tolerance value +/- value
  273. if self.tol_minimum == self.tol_maximum:
  274. self.tol_text = PLUS_MINUS + self.format_tolerance_text(abs(self.tol_maximum))
  275. self.tol_text_height = self.tol_char_height
  276. self.tol_text_width = self.tolerance_text_width(len(self.tol_text))
  277. else: # 2 stacked values: +upper tolerance <above> -lower tolerance
  278. self.tol_text_upper = sign_char(self.tol_maximum) + self.format_tolerance_text(
  279. abs(self.tol_maximum))
  280. self.tol_text_lower = sign_char(self.tol_minimum * -1) + self.format_tolerance_text(
  281. abs(self.tol_minimum))
  282. # requires 2 text lines
  283. self.tol_text_height = self.tol_char_height + (self.tol_text_height * self.tol_line_spacing)
  284. self.tol_text_width = self.tolerance_text_width(max(len(self.tol_text_upper), len(self.tol_text_lower)))
  285. # reset text height
  286. self.text_height = max(self.text_height, self.tol_text_height)
  287. elif self.dim_limits:
  288. self.tol_text = None # always None for limits
  289. # limits text is always 2 stacked numbers and requires actual measurement
  290. self.tol_text_upper = None # text for upper limit
  291. self.tol_text_lower = None # text for lower limit
  292. self.tol_text_height = self.tol_char_height + (self.tol_text_height * self.tol_line_spacing)
  293. self.tol_text_width = None # requires actual measurement
  294. self.text_height = max(self.text_height, self.tol_text_height)
  295. @property
  296. def text_inside(self):
  297. return not self.text_outside
  298. def render(self, block: 'GenericLayoutType'): # interface definition
  299. self.block = block
  300. # tolerance requires MTEXT support, switch of rendering of tolerances and limits
  301. if not self.supports_dxf_r2000:
  302. self.dim_tolerance = 0
  303. self.dim_limits = 0
  304. @property
  305. def char_height(self) -> float:
  306. """
  307. Unscaled (self.dim_scale) character height defined by text style or DIMTXT.
  308. Hint: Use self.text_height for proper scaled text height in drawing units.
  309. """
  310. height = self.text_style.get_dxf_attrib('height', 0) # type: float
  311. if height == 0: # variable text height (not fixed)
  312. height = self.dim_style.get('dimtxt', 1.)
  313. return height
  314. def text_width(self, text: str) -> float:
  315. """
  316. Return width of `text` in drawing units.
  317. """
  318. char_width = self.text_height * self.text_width_factor # type: float
  319. return len(text) * char_width
  320. def tolerance_text_width(self, count: int) -> float:
  321. """
  322. Return width of `count` characters in drawing units.
  323. """
  324. return self.tol_text_height * self.text_width_factor * count
  325. def default_attributes(self) -> dict:
  326. """
  327. Returns default DXF attributes as dict.
  328. """
  329. return {
  330. 'layer': self.default_layer, # type: str
  331. 'color': self.default_color, # type: int
  332. }
  333. def wcs(self, point: 'Vertex') -> Vector:
  334. """
  335. Transform `point` in UCS coordinates into WCS coordinates.
  336. """
  337. return self.ucs.to_wcs(point)
  338. def ocs(self, point: 'Vertex') -> Vector:
  339. """
  340. Transform `point` in UCS coordinates into OCS coordinates.
  341. """
  342. return self.ucs.to_ocs(point)
  343. def to_ocs_angle(self, angle: float) -> float:
  344. """
  345. Transform `angle` from UCS to OCS.
  346. """
  347. return self.ucs.to_ocs_angle_deg(angle)
  348. def text_override(self, measurement: float) -> str:
  349. """
  350. Create dimension text for `measurement` in drawing units and applies text overriding properties.
  351. """
  352. text = self.dimension.dxf.text # type: str
  353. if text == ' ': # suppress text
  354. return ''
  355. elif text == '' or text == '<>': # measured distance
  356. return self.format_text(measurement)
  357. else: # user override
  358. return text
  359. def format_text(self, value: float) -> str:
  360. """
  361. Rounding and text formatting of `value`, removes leading and trailing zeros if necessary.
  362. """
  363. return format_text(
  364. value,
  365. self.text_round,
  366. self.text_decimal_places,
  367. self.text_suppress_zeros,
  368. self.text_decimal_separator,
  369. self.text_format,
  370. )
  371. def compile_mtext(self) -> str:
  372. text = self.text
  373. if self.dim_tolerance:
  374. align = max(int(self.tol_valign), 0)
  375. align = min(align, 2)
  376. if self.tol_text is None:
  377. text = TOLERANCE_TEMPLATE2.format(
  378. align=align,
  379. txt=text,
  380. fac=self.tol_text_scale_factor,
  381. upr=self.tol_text_upper,
  382. lwr=self.tol_text_lower,
  383. )
  384. else:
  385. text = TOLERANCE_TEMPLATE1.format(
  386. align=align,
  387. txt=text,
  388. fac=self.tol_text_scale_factor,
  389. tol=self.tol_text,
  390. )
  391. elif self.dim_limits:
  392. text = LIMITS_TEMPLATE.format(
  393. upr=self.tol_text_upper,
  394. lwr=self.tol_text_lower,
  395. fac=self.tol_text_scale_factor,
  396. )
  397. return text
  398. def format_tolerance_text(self, value: float) -> str:
  399. """
  400. Rounding and text formatting of tolerance `value`, removes leading and trailing zeros if necessary.
  401. """
  402. return format_text(
  403. value=value,
  404. dimrnd=None,
  405. dimdec=self.tol_decimal_places,
  406. dimzin=self.tol_suppress_zeros,
  407. dimdsep=self.text_decimal_separator,
  408. )
  409. def location_override(self, location: 'Vertex', leader=False, relative=False) -> None:
  410. """
  411. Set user defined dimension text location. ezdxf defines a user defined location per definition as 'outside'.
  412. Args:
  413. location: text midpoint
  414. leader: use leader or not (movement rules)
  415. relative: is location absolut (in UCS) or relative to dimension line center.
  416. """
  417. self.dim_style.set_location(location, leader, relative)
  418. self.user_location = Vec2(location)
  419. self.text_movement_rule = 1 if leader else 2
  420. self.relative_user_location = relative
  421. self.text_outside = True
  422. def add_line(self, start: 'Vertex', end: 'Vertex', dxfattribs: dict = None, remove_hidden_lines=False) -> None:
  423. """
  424. Add a LINE entity to the dimension BLOCK. Removes parts of the line hidden by dimension text if
  425. `remove_hidden_lines` is True.
  426. Args:
  427. start: start point of line
  428. end: end point of line
  429. dxfattribs: additional or overridden DXF attributes
  430. remove_hidden_lines: removes parts of the line hidden by dimension text if True
  431. """
  432. def order(a: Vec2, b: Vec2) -> Tuple[Vec2, Vec2]:
  433. if (start - a).magnitude < (start - b).magnitude:
  434. return a, b
  435. else:
  436. return b, a
  437. attribs = self.default_attributes()
  438. if dxfattribs:
  439. attribs.update(dxfattribs)
  440. text_box = self.text_box
  441. wcs = self.ucs.to_wcs
  442. if remove_hidden_lines and (text_box is not None):
  443. start_inside = int(text_box.is_inside(start))
  444. end_inside = int(text_box.is_inside(end))
  445. inside = start_inside + end_inside
  446. if inside == 2: # start and end inside text_box
  447. return # do not draw line
  448. elif inside == 1: # one point inside text_box
  449. intersection_points = text_box.intersect(ConstructionLine(start, end))
  450. # one point inside one point outside -> one intersection point
  451. p1 = intersection_points[0]
  452. p2 = start if start_inside else end
  453. self.block.add_line(wcs(p1), wcs(p2), dxfattribs=attribs)
  454. return
  455. else:
  456. intersection_points = text_box.intersect(ConstructionLine(start, end))
  457. if len(intersection_points) == 2:
  458. # sort intersection points by distance to start point
  459. p1, p2 = order(intersection_points[0], intersection_points[1])
  460. # line[start-p1] - gap - line[p2-end]
  461. self.block.add_line(wcs(start), wcs(p1), dxfattribs=attribs)
  462. self.block.add_line(wcs(p2), wcs(end), dxfattribs=attribs)
  463. return
  464. # else: fall trough
  465. self.block.add_line(wcs(start), wcs(end), dxfattribs=attribs)
  466. def add_blockref(self, name: str, insert: 'Vertex', rotation: float = 0,
  467. scale: float = 1., dxfattribs: dict = None) -> None:
  468. """
  469. Add block references and standard arrows to the dimension BLOCK.
  470. Args:
  471. name: block or arrow name
  472. insert: insertion point in UCS
  473. rotation: rotation angle in degrees in UCS (x-axis is 0 degrees)
  474. scale: scaling factor for x- and y-direction
  475. dxfattribs: additional or overridden DXF attributes
  476. """
  477. attribs = self.default_attributes()
  478. insert = self.ocs(insert)
  479. rotation = self.to_ocs_angle(rotation)
  480. if self.requires_extrusion:
  481. attribs['extrusion'] = self.ucs.uz
  482. if name in ARROWS: # generates automatically BLOCK definitions for arrows if needed
  483. if dxfattribs:
  484. attribs.update(dxfattribs)
  485. self.block.add_arrow_blockref(name, insert=insert, size=scale, rotation=rotation, dxfattribs=attribs)
  486. else:
  487. if name not in self.drawing.blocks:
  488. raise DXFUndefinedBlockError('Undefined block: "{}"'.format(name))
  489. attribs['rotation'] = rotation
  490. if scale != 1.:
  491. attribs['xscale'] = scale
  492. attribs['yscale'] = scale
  493. if dxfattribs:
  494. attribs.update(dxfattribs)
  495. self.block.add_blockref(name, insert=insert, dxfattribs=attribs)
  496. def add_text(self, text: str, pos: Vector, rotation: float, dxfattribs: dict = None) -> None:
  497. """
  498. Add TEXT (DXF R12) or MTEXT (DXF R2000+) entity to the dimension BLOCK.
  499. Args:
  500. text: text as string
  501. pos: insertion location in UCS
  502. rotation: rotation angle in degrees in UCS (x-axis is 0 degrees)
  503. dxfattribs: additional or overridden DXF attributes
  504. """
  505. attribs = self.default_attributes()
  506. attribs['style'] = self.text_style_name
  507. attribs['color'] = self.text_color
  508. if self.requires_extrusion:
  509. attribs['extrusion'] = self.ucs.uz
  510. if self.supports_dxf_r2000:
  511. text_direction = self.ucs.to_wcs(Vec2.from_deg_angle(rotation)) - self.ucs.origin
  512. attribs['text_direction'] = text_direction
  513. attribs['char_height'] = self.text_height
  514. attribs['insert'] = self.wcs(pos)
  515. attribs['attachment_point'] = self.text_attachment_point
  516. if self.supports_dxf_r2007:
  517. if self.text_fill:
  518. attribs['box_fill_scale'] = self.text_box_fill_scale
  519. attribs['bg_fill_color'] = self.text_fill_color
  520. attribs['bg_fill'] = 3 if self.text_fill == 1 else 1
  521. if dxfattribs:
  522. attribs.update(dxfattribs)
  523. self.block.add_mtext(text, dxfattribs=attribs)
  524. else:
  525. attribs['rotation'] = self.ucs.to_ocs_angle_deg(rotation)
  526. attribs['height'] = self.text_height
  527. if dxfattribs:
  528. attribs.update(dxfattribs)
  529. dxftext = self.block.add_text(text, dxfattribs=attribs)
  530. dxftext.set_pos(self.ocs(pos), align='MIDDLE_CENTER')
  531. def add_defpoints(self, points: Iterable['Vertex']) -> None:
  532. """
  533. Add POINT entities at layer 'DEFPOINTS' for all points in `points`.
  534. """
  535. attribs = {
  536. 'layer': 'DEFPOINTS',
  537. }
  538. for point in points:
  539. self.block.add_point(self.wcs(point), dxfattribs=attribs)
  540. def add_leader(self, p1: Vec2, p2: Vec2, p3: Vec2, dxfattribs: dict = None):
  541. """
  542. Add simple leader line from p1 to p2 to p3.
  543. Args:
  544. p1: target point
  545. p2: first text point
  546. p3: second text point
  547. dxfattribs: DXF attribute
  548. """
  549. self.add_line(p1, p2, dxfattribs)
  550. self.add_line(p2, p3, dxfattribs)
  551. def transform_ucs_to_wcs(self) -> None:
  552. """
  553. Transforms dimension definition points into WCS or if required into OCS.
  554. Can not be called in __init__(), because inherited classes may be need unmodified values.
  555. """
  556. def from_ucs(attr, func):
  557. point = self.dimension.get_dxf_attrib(attr)
  558. self.dimension.set_dxf_attrib(attr, func(point))
  559. from_ucs('defpoint', self.wcs)
  560. from_ucs('defpoint2', self.wcs)
  561. from_ucs('defpoint3', self.wcs)
  562. from_ucs('text_midpoint', self.ocs)
  563. self.dimension.dxf.angle = self.ucs.to_ocs_angle_deg(self.dimension.dxf.angle)
  564. def finalize(self) -> None:
  565. self.transform_ucs_to_wcs()
  566. def order_leader_points(p1: Vec2, p2: Vec2, p3: Vec2) -> Tuple[Vec2, Vec2]:
  567. if (p1 - p2).magnitude > (p1 - p3).magnitude:
  568. return p3, p2
  569. else:
  570. return p2, p3
  571. class LinearDimension(BaseDimensionRenderer):
  572. """
  573. Linear dimension line renderer, used for horizontal, vertical, rotated and aligned DIMENSION entities.
  574. Args:
  575. dimension: DXF entity DIMENSION
  576. ucs: user defined coordinate system
  577. override: dimension style override management object
  578. """
  579. def __init__(self, dimension: 'Dimension', ucs: 'UCS' = None, override: 'DimStyleOverride' = None):
  580. super().__init__(dimension, ucs, override)
  581. self.oblique_angle = self.dimension.get_dxf_attrib('oblique_angle', 90) # type: float
  582. self.dim_line_angle = self.dimension.get_dxf_attrib('angle', 0) # type: float
  583. self.dim_line_angle_rad = math.radians(self.dim_line_angle) # type: float
  584. self.ext_line_angle = self.dim_line_angle + self.oblique_angle # type: float
  585. self.ext_line_angle_rad = math.radians(self.ext_line_angle) # type: float
  586. # text is aligned to dimension line
  587. self.text_rotation = self.dim_line_angle # type: float
  588. if self.text_halign in (3, 4): # text above extension line, is always aligned with extension lines
  589. self.text_rotation = self.ext_line_angle
  590. self.ext1_line_start = Vec2(self.dimension.dxf.defpoint2)
  591. self.ext2_line_start = Vec2(self.dimension.dxf.defpoint3)
  592. ext1_ray = ConstructionRay(self.ext1_line_start, angle=self.ext_line_angle_rad)
  593. ext2_ray = ConstructionRay(self.ext2_line_start, angle=self.ext_line_angle_rad)
  594. dim_line_ray = ConstructionRay(self.dimension.dxf.defpoint, angle=self.dim_line_angle_rad)
  595. self.dim_line_start = dim_line_ray.intersect(ext1_ray) # type: Vec2
  596. self.dim_line_end = dim_line_ray.intersect(ext2_ray) # type: Vec2
  597. self.dim_line_center = self.dim_line_start.lerp(self.dim_line_end) # type: Vec2
  598. if self.dim_line_start == self.dim_line_end:
  599. self.dim_line_vec = Vec2.from_angle(self.dim_line_angle_rad)
  600. else:
  601. self.dim_line_vec = (self.dim_line_end - self.dim_line_start).normalize() # type: Vec2
  602. # set dimension defpoint to expected location - 3D vertex required!
  603. self.dimension.dxf.defpoint = Vector(self.dim_line_start)
  604. self.measurement = (self.dim_line_end - self.dim_line_start).magnitude # type: float
  605. self.text = self.text_override(self.measurement * self.dim_measurement_factor) # type: str
  606. # only for linear dimension in multi point mode
  607. self.multi_point_mode = override.pop('multi_point_mode', False)
  608. # 1 .. move wide text up
  609. # 2 .. move wide text down
  610. # None .. ignore
  611. self.move_wide_text = override.pop('move_wide_text', None) # type: bool
  612. # actual text width in drawing units
  613. self.dim_text_width = 0 # type: float
  614. # arrows
  615. self.required_arrows_space = 2 * self.arrow_size + self.text_gap # type: float
  616. self.arrows_outside = self.required_arrows_space > self.measurement # type: bool
  617. # text location and rotation
  618. if self.text:
  619. # text width and required space
  620. self.dim_text_width = self.text_width(self.text) # type: float
  621. if self.dim_tolerance:
  622. self.dim_text_width += self.tol_text_width
  623. elif self.dim_limits:
  624. # limits show the upper and lower limit of the measurement as stacked values
  625. # and with the size of tolerances
  626. measurement = self.measurement * self.dim_measurement_factor
  627. self.measurement_upper_limit = measurement + self.tol_maximum
  628. self.measurement_lower_limit = measurement - self.tol_minimum
  629. self.tol_text_upper = self.format_tolerance_text(self.measurement_upper_limit)
  630. self.tol_text_lower = self.format_tolerance_text(self.measurement_lower_limit)
  631. self.tol_text_width = self.tolerance_text_width(max(len(self.tol_text_upper), len(self.tol_text_lower)))
  632. # only limits are displayed so:
  633. self.dim_text_width = self.tol_text_width
  634. if self.multi_point_mode:
  635. # ezdxf has total control about vertical text position in multi point mode
  636. self.text_vertical_position = 0.
  637. if self.text_valign == 0 and abs(self.text_vertical_position) < 0.7:
  638. # vertical centered text needs also space for arrows
  639. required_space = self.dim_text_width + 2 * self.arrow_size
  640. else:
  641. required_space = self.dim_text_width
  642. self.is_wide_text = required_space > self.measurement
  643. if not self.force_text_inside:
  644. # place text outside if wide text and not forced inside
  645. self.text_outside = self.is_wide_text
  646. elif self.is_wide_text and self.text_halign < 3:
  647. # center wide text horizontal
  648. self.text_halign = 0
  649. # use relative text shift to move wide text up or down in multi point mode
  650. if self.multi_point_mode and self.is_wide_text and self.move_wide_text:
  651. shift_value = self.text_height + self.text_gap
  652. if self.move_wide_text == 1: # move text up
  653. self.text_shift_v = shift_value
  654. if self.vertical_placement == -1: # text below dimension line
  655. # shift again
  656. self.text_shift_v += shift_value
  657. elif self.move_wide_text == 2: # move text down
  658. self.text_shift_v = -shift_value
  659. if self.vertical_placement == 1: # text above dimension line
  660. # shift again
  661. self.text_shift_v -= shift_value
  662. # get final text location - no altering after this line
  663. self.text_location = self.get_text_location() # type: Vec2
  664. # text rotation override
  665. rotation = self.text_rotation # type: float
  666. if self.user_text_rotation is not None:
  667. rotation = self.user_text_rotation
  668. elif self.text_outside and self.text_outside_horizontal:
  669. rotation = 0
  670. elif self.text_inside and self.text_inside_horizontal:
  671. rotation = 0
  672. self.text_rotation = rotation
  673. self.text_box = TextBox(
  674. center=self.text_location,
  675. width=self.dim_text_width,
  676. height=self.text_height,
  677. angle=self.text_rotation,
  678. gap=self.text_gap * .75
  679. )
  680. if self.text_has_leader:
  681. p1, p2, *_ = self.text_box.corners
  682. self.leader1, self.leader2 = order_leader_points(self.dim_line_center, p1, p2)
  683. # not exact what BricsCAD (AutoCAD) expect, but close enough
  684. self.dimension.dxf.text_midpoint = self.leader1
  685. else:
  686. # write final text location into DIMENSION entity
  687. self.dimension.dxf.text_midpoint = self.text_location
  688. @property
  689. def has_relative_text_movement(self):
  690. return bool(self.text_shift_h or self.text_shift_v)
  691. def apply_text_shift(self, location: Vec2, text_rotation: float) -> Vec2:
  692. """
  693. Add `self.text_shift_h` and `sel.text_shift_v` to point `location`, shifting along and perpendicular to
  694. text orientation defined by `text_rotation`
  695. Args:
  696. location: location point
  697. text_rotation: text rotation in degrees
  698. Returns: new location
  699. """
  700. shift_vec = Vec2((self.text_shift_h, self.text_shift_v))
  701. location += shift_vec.rotate(text_rotation)
  702. return location
  703. def render(self, block: 'GenericLayoutType') -> None:
  704. """
  705. Main method to create dimension geometry as basic DXF entities in the associated BLOCK layout.
  706. Args:
  707. block: target BLOCK for rendering
  708. """
  709. # call required to setup some requirements
  710. super().render(block)
  711. # add extension line 1
  712. if not self.suppress_ext1_line:
  713. above_ext_line1 = self.text_halign == 3
  714. start, end = self.extension_line_points(self.ext1_line_start, self.dim_line_start, above_ext_line1)
  715. self.add_extension_line(start, end, linetype=self.ext1_linetype_name)
  716. # add extension line 2
  717. if not self.suppress_ext2_line:
  718. above_ext_line2 = self.text_halign == 4
  719. start, end = self.extension_line_points(self.ext2_line_start, self.dim_line_end, above_ext_line2)
  720. self.add_extension_line(start, end, linetype=self.ext2_linetype_name)
  721. # add arrow symbols (block references), also adjust dimension line start and end point
  722. dim_line_start, dim_line_end = self.add_arrows()
  723. # add dimension line
  724. self.add_dimension_line(dim_line_start, dim_line_end)
  725. # add measurement text as last entity to see text fill properly
  726. if self.text:
  727. if self.supports_dxf_r2000:
  728. text = self.compile_mtext()
  729. else:
  730. text = self.text
  731. self.add_measurement_text(text, self.text_location, self.text_rotation)
  732. if self.text_has_leader:
  733. self.add_leader(self.dim_line_center, self.leader1, self.leader2)
  734. # add POINT entities at definition points
  735. self.add_defpoints([self.dim_line_start, self.ext1_line_start, self.ext2_line_start])
  736. def get_text_location(self) -> Vec2:
  737. """
  738. Get text midpoint in UCS from user defined location or default text location.
  739. """
  740. # apply relative text shift as user location override without leader
  741. if self.has_relative_text_movement:
  742. location = self.default_text_location()
  743. location = self.apply_text_shift(location, self.text_rotation)
  744. self.location_override(location)
  745. if self.user_location is not None:
  746. location = self.user_location
  747. if self.relative_user_location:
  748. location = self.dim_line_center + location
  749. # define overridden text location as outside
  750. self.text_outside = True
  751. else:
  752. location = self.default_text_location()
  753. return location
  754. def default_text_location(self) -> Vec2:
  755. """
  756. Calculate default text location in UCS based on `self.text_halign`, `self.text_valign` and `self.text_outside`
  757. """
  758. start = self.dim_line_start
  759. end = self.dim_line_end
  760. halign = self.text_halign
  761. # positions the text above and aligned with the first/second extension line
  762. if halign in (3, 4):
  763. # horizontal location
  764. hdist = self.text_gap + self.text_height / 2.
  765. hvec = self.dim_line_vec * hdist
  766. location = (start if halign == 3 else end) - hvec
  767. # vertical location
  768. vdist = self.ext_line_extension + self.dim_text_width / 2.
  769. location += Vec2.from_deg_angle(self.ext_line_angle).normalize(vdist)
  770. else:
  771. # relocate outside text to center location
  772. if self.text_outside:
  773. halign = 0
  774. if halign == 0:
  775. location = self.dim_line_center # center of dimension line
  776. else:
  777. hdist = self.dim_text_width / 2. + self.arrow_size + self.text_gap
  778. if halign == 1: # positions the text next to the first extension line
  779. location = start + (self.dim_line_vec * hdist)
  780. else: # positions the text next to the second extension line
  781. location = end - (self.dim_line_vec * hdist)
  782. if self.text_outside: # move text up
  783. vdist = self.ext_line_extension + self.text_gap + self.text_height / 2.
  784. else:
  785. # distance from extension line to text midpoint
  786. vdist = self.text_vertical_distance()
  787. location += self.dim_line_vec.orthogonal().normalize(vdist)
  788. return location
  789. def add_arrows(self) -> Tuple[Vec2, Vec2]:
  790. """
  791. Add arrows or ticks to dimension.
  792. Returns: dimension line connection points
  793. """
  794. attribs = {
  795. 'color': self.dim_line_color,
  796. }
  797. start = self.dim_line_start
  798. end = self.dim_line_end
  799. outside = self.arrows_outside
  800. arrow1 = not self.suppress_arrow1
  801. arrow2 = not self.suppress_arrow2
  802. if self.tick_size > 0.: # oblique stroke, but double the size
  803. if arrow1:
  804. self.add_blockref(
  805. ARROWS.oblique,
  806. insert=start,
  807. rotation=self.dim_line_angle,
  808. scale=self.tick_size * 2,
  809. dxfattribs=attribs,
  810. )
  811. if arrow2:
  812. self.add_blockref(
  813. ARROWS.oblique,
  814. insert=end,
  815. rotation=self.dim_line_angle,
  816. scale=self.tick_size * 2,
  817. dxfattribs=attribs,
  818. )
  819. else:
  820. scale = self.arrow_size
  821. start_angle = self.dim_line_angle + 180.
  822. end_angle = self.dim_line_angle
  823. if outside:
  824. start_angle, end_angle = end_angle, start_angle
  825. if arrow1:
  826. self.add_blockref(self.arrow1_name, insert=start, scale=scale, rotation=start_angle,
  827. dxfattribs=attribs) # reverse
  828. if arrow2:
  829. self.add_blockref(self.arrow2_name, insert=end, scale=scale, rotation=end_angle, dxfattribs=attribs)
  830. if not outside:
  831. # arrows inside extension lines: adjust connection points for the remaining dimension line
  832. if arrow1:
  833. start = connection_point(self.arrow1_name, start, scale, start_angle)
  834. if arrow2:
  835. end = connection_point(self.arrow2_name, end, scale, end_angle)
  836. else:
  837. # add additional extension lines to arrows placed outside of dimension extension lines
  838. self.add_arrow_extension_lines()
  839. return start, end
  840. def add_arrow_extension_lines(self):
  841. """
  842. Add extension lines to arrows placed outside of dimension extension lines. Called by `self.add_arrows()`.
  843. """
  844. def has_arrow_extension(name: str) -> bool:
  845. return (name is not None) and (name in ARROWS) and (name not in ARROWS.ORIGIN_ZERO)
  846. attribs = {
  847. 'color': self.dim_line_color,
  848. }
  849. start = self.dim_line_start
  850. end = self.dim_line_end
  851. arrow_size = self.arrow_size
  852. if not self.suppress_arrow1 and has_arrow_extension(self.arrow1_name):
  853. self.add_line(
  854. start - self.dim_line_vec * arrow_size,
  855. start - self.dim_line_vec * (2 * arrow_size),
  856. dxfattribs=attribs,
  857. )
  858. if not self.suppress_arrow2 and has_arrow_extension(self.arrow2_name):
  859. self.add_line(
  860. end + self.dim_line_vec * arrow_size,
  861. end + self.dim_line_vec * (2 * arrow_size),
  862. dxfattribs=attribs,
  863. )
  864. def add_measurement_text(self, dim_text: str, pos: Vec2, rotation: float) -> None:
  865. """
  866. Add measurement text to dimension BLOCK.
  867. Args:
  868. dim_text: dimension text
  869. pos: text location
  870. rotation: text rotation in degrees
  871. """
  872. attribs = {
  873. 'color': self.text_color,
  874. }
  875. self.add_text(dim_text, pos=Vector(pos), rotation=rotation, dxfattribs=attribs)
  876. def add_dimension_line(self, start: 'Vertex', end: 'Vertex') -> None:
  877. """
  878. Add dimension line to dimension BLOCK, adds extension DIMDLE if required, and uses DIMSD1 or DIMSD2 to suppress
  879. first or second part of dimension line. Removes line parts hidden by dimension text.
  880. Args:
  881. start: dimension line start
  882. end: dimension line end
  883. """
  884. extension = self.dim_line_vec * self.dim_line_extension
  885. if self.arrow1_name is None or ARROWS.has_extension_line(self.arrow1_name):
  886. start = start - extension
  887. if self.arrow2_name is None or ARROWS.has_extension_line(self.arrow2_name):
  888. end = end + extension
  889. attribs = {
  890. 'color': self.dim_line_color
  891. }
  892. if self.dim_linetype is not None:
  893. attribs['linetype'] = self.dim_linetype
  894. if self.supports_dxf_r2000:
  895. attribs['lineweight'] = self.dim_lineweight
  896. if self.suppress_dim1_line or self.suppress_dim2_line:
  897. if not self.suppress_dim1_line:
  898. self.add_line(start, self.dim_line_center, dxfattribs=attribs, remove_hidden_lines=True)
  899. if not self.suppress_dim2_line:
  900. self.add_line(self.dim_line_center, end, dxfattribs=attribs, remove_hidden_lines=True)
  901. else:
  902. self.add_line(start, end, dxfattribs=attribs, remove_hidden_lines=True)
  903. def extension_line_points(self, start: Vec2, end: Vec2, text_above_extline=False) -> Tuple[Vec2, Vec2]:
  904. """
  905. Adjust start and end point of extension line by dimension variables DIMEXE, DIMEXO, DIMEXFIX, DIMEXLEN.
  906. Args:
  907. start: start point of extension line (measurement point)
  908. end: end point at dimension line
  909. text_above_extline: True if text is above and aligned with extension line
  910. Returns: adjusted start and end point
  911. """
  912. if start == end:
  913. direction = Vec2.from_deg_angle(self.ext_line_angle)
  914. else:
  915. direction = (end - start).normalize()
  916. if self.ext_line_fixed:
  917. start = end - (direction * self.ext_line_length)
  918. else:
  919. start = start + direction * self.ext_line_offset
  920. extension = self.ext_line_extension
  921. if text_above_extline:
  922. extension += self.dim_text_width
  923. end = end + direction * extension
  924. return start, end
  925. def add_extension_line(self, start: 'Vertex', end: 'Vertex', linetype: str = None) -> None:
  926. """
  927. Add extension lines from dimension line to measurement point.
  928. """
  929. attribs = {
  930. 'color': self.ext_line_color
  931. }
  932. if linetype is not None:
  933. attribs['linetype'] = linetype
  934. # lineweight requires DXF R2000 or later
  935. if self.supports_dxf_r2000:
  936. attribs['lineweight'] = self.ext_lineweight
  937. self.add_line(start, end, dxfattribs=attribs)
  938. @property
  939. def vertical_placement(self) -> float:
  940. """
  941. Returns vertical placement of dimension text as 1 for above, 0 for center and -1 for below dimension line.
  942. """
  943. if self.text_valign == 0:
  944. return 0
  945. elif self.text_valign == 4:
  946. return -1
  947. else:
  948. return 1
  949. def text_vertical_distance(self) -> float:
  950. """
  951. Returns the vertical distance for dimension line to text midpoint. Positive values are above the line, negative
  952. values are below the line.
  953. """
  954. if self.text_valign == 0:
  955. return self.text_height * self.text_vertical_position
  956. else:
  957. return (self.text_height / 2. + self.text_gap) * self.vertical_placement
  958. class DimensionRenderer:
  959. def dispatch(self, override: 'DimStyleOverride', ucs: 'UCS') -> BaseDimensionRenderer:
  960. dimension = override.dimension
  961. dim_type = dimension.dim_type
  962. if dim_type in (0, 1):
  963. return self.linear(dimension, ucs, override)
  964. elif dim_type == 2:
  965. return self.angular(dimension, ucs, override)
  966. elif dim_type == 3:
  967. return self.diameter(dimension, ucs, override)
  968. elif dim_type == 4:
  969. return self.radius(dimension, ucs, override)
  970. elif dim_type == 5:
  971. return self.angular3p(dimension, ucs, override)
  972. elif dim_type == 6:
  973. return self.ordinate(dimension, ucs, override)
  974. else:
  975. raise DXFValueError("Unknown DIMENSION type: {}".format(dim_type))
  976. def linear(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
  977. """
  978. Call renderer for linear dimension lines: horizontal, vertical and rotated
  979. """
  980. return LinearDimension(dimension, ucs, override)
  981. def angular(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
  982. raise NotImplemented
  983. def diameter(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
  984. raise NotImplemented
  985. def radius(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
  986. raise NotImplemented
  987. def angular3p(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
  988. raise NotImplemented
  989. def ordinate(self, dimension: 'Dimension', ucs: 'UCS', override: 'DimStyleOverride' = None):
  990. raise NotImplemented
  991. def format_text(value: float, dimrnd: float = None, dimdec: int = None, dimzin: int = 0, dimdsep: str = '.',
  992. dimpost: str = '<>') -> str:
  993. if dimrnd is not None:
  994. value = xround(value, dimrnd)
  995. if dimdec is None:
  996. fmt = "{:f}"
  997. dimzin = dimzin | 8 # remove pending zeros for undefined decimal places, '{:f}'.format(0) -> '0.000000'
  998. else:
  999. fmt = "{:." + str(dimdec) + "f}"
  1000. text = fmt.format(value)
  1001. leading = bool(dimzin & 4)
  1002. pending = bool(dimzin & 8)
  1003. text = suppress_zeros(text, leading, pending)
  1004. if dimdsep != '.':
  1005. text = text.replace('.', dimdsep)
  1006. if dimpost:
  1007. if '<>' in dimpost:
  1008. fmt = dimpost.replace('<>', '{}', 1)
  1009. text = fmt.format(text)
  1010. else:
  1011. raise DXFValueError('Invalid dimpost string: "{}"'.format(dimpost))
  1012. return text
  1013. CAN_SUPPRESS_ARROW1 = {
  1014. ARROWS.dot,
  1015. ARROWS.dot_small,
  1016. ARROWS.dot_blank,
  1017. ARROWS.origin_indicator,
  1018. ARROWS.origin_indicator_2,
  1019. ARROWS.dot_smallblank,
  1020. ARROWS.none,
  1021. ARROWS.oblique,
  1022. ARROWS.box_filled,
  1023. ARROWS.box,
  1024. ARROWS.integral,
  1025. ARROWS.architectural_tick,
  1026. }
  1027. def sign_char(value: float) -> str:
  1028. if value < 0.:
  1029. return '-'
  1030. elif value > 0:
  1031. return '+'
  1032. else:
  1033. return ' '
  1034. def sort_projected_points(points: Iterable['Vertex'], angle: float = 0) -> List[Vec2]:
  1035. direction = Vec2.from_deg_angle(angle)
  1036. projected_vectors = [(direction.project(Vec2(p)), p) for p in points]
  1037. return [p for projection, p in sorted(projected_vectors)]
  1038. def multi_point_linear_dimension(
  1039. layout: 'GenericLayoutType',
  1040. base: 'Vertex',
  1041. points: Iterable['Vertex'],
  1042. angle: float = 0,
  1043. ucs: 'UCS' = None,
  1044. avoid_double_rendering: bool = True,
  1045. dimstyle: str = 'EZDXF',
  1046. override: dict = None,
  1047. dxfattribs: dict = None,
  1048. discard=False) -> None:
  1049. """
  1050. Creates multiple DIMENSION entities for each point pair in `points`. Measurement points will be sorted by appearance
  1051. on the dimension line vector.
  1052. Args:
  1053. layout: target layout (model space, paper space or block)
  1054. base: base point, any point on the dimension line vector will do
  1055. points: iterable of measurement points
  1056. angle: dimension line rotation in degrees (0=horizontal, 90=vertical)
  1057. ucs: user defined coordinate system
  1058. avoid_double_rendering: removes first extension line and arrow of following DIMENSION entity
  1059. dimstyle: dimension style name
  1060. override: dictionary of overridden dimension style attributes
  1061. dxfattribs: DXF attributes for DIMENSION entities
  1062. discard: discard rendering result for friendly CAD applications like BricsCAD to get a native and likely better
  1063. rendering result. (does not work with AutoCAD)
  1064. """
  1065. def suppress_arrow1(dimstyle_override) -> bool:
  1066. arrow_name1, arrow_name2 = dimstyle_override.get_arrow_names()
  1067. if (arrow_name1 is None) or (arrow_name1 in CAN_SUPPRESS_ARROW1):
  1068. return True
  1069. else:
  1070. return False
  1071. points = sort_projected_points(points, angle)
  1072. base = Vec2(base)
  1073. override = override or {}
  1074. override['dimtix'] = 1 # do not place measurement text outside
  1075. override['dimtvp'] = 0 # do not place measurement text outside
  1076. override['multi_point_mode'] = True
  1077. # 1 .. move wide text up; 2 .. move wide text down; None .. ignore
  1078. # moving text down, looks best combined with text fill bg: DIMTFILL = 1
  1079. move_wide_text = 1
  1080. _suppress_arrow1 = False
  1081. first_run = True
  1082. for p1, p2 in zip(points[:-1], points[1:]):
  1083. _override = dict(override)
  1084. _override['move_wide_text'] = move_wide_text
  1085. if avoid_double_rendering and not first_run:
  1086. _override['dimse1'] = 1
  1087. _override['suppress_arrow1'] = _suppress_arrow1
  1088. style = layout.add_linear_dim(
  1089. Vector(base),
  1090. Vector(p1),
  1091. Vector(p2),
  1092. angle=angle,
  1093. dimstyle=dimstyle,
  1094. override=_override,
  1095. dxfattribs=dxfattribs,
  1096. )
  1097. if first_run:
  1098. _suppress_arrow1 = suppress_arrow1(style)
  1099. renderer = cast(LinearDimension, style.render(ucs, discard=discard))
  1100. if renderer.is_wide_text:
  1101. # after wide text switch moving direction
  1102. if move_wide_text == 1:
  1103. move_wide_text = 2
  1104. else:
  1105. move_wide_text = 1
  1106. else: # reset to move text up
  1107. move_wide_text = 1
  1108. first_run = False