hatch.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. # Created: 24.05.2015
  2. # Copyright (c) 2015-2018, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, Optional, List, Tuple, Sequence, Union, Iterable
  5. import math
  6. from contextlib import contextmanager
  7. from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
  8. from ezdxf.lldxf.types import DXFTag, DXFVertex
  9. from ezdxf.lldxf.tags import DXFStructureError, group_tags, Tags
  10. from ezdxf.lldxf.extendedtags import ExtendedTags
  11. from ezdxf.lldxf import const
  12. from ezdxf.tools.pattern import PATTERN # acad standard pattern definitions
  13. from ezdxf.tools.rgb import rgb2int, int2rgb
  14. from ezdxf.lldxf.const import DXFValueError, DXFVersionError
  15. from ezdxf.math.bspline import bspline_control_frame
  16. from .graphics import none_subclass, entity_subclass, ModernGraphicEntity
  17. if TYPE_CHECKING:
  18. from ezdxf.eztypes import RGB
  19. _HATCH_TPL = """0
  20. HATCH
  21. 5
  22. 0
  23. 330
  24. 0
  25. 100
  26. AcDbEntity
  27. 8
  28. 0
  29. 62
  30. 1
  31. 100
  32. AcDbHatch
  33. 10
  34. 0.0
  35. 20
  36. 0.0
  37. 30
  38. 0.0
  39. 210
  40. 0.0
  41. 220
  42. 0.0
  43. 230
  44. 1.0
  45. 2
  46. SOLID
  47. 70
  48. 1
  49. 71
  50. 0
  51. 91
  52. 0
  53. 75
  54. 1
  55. 76
  56. 1
  57. 98
  58. 1
  59. 10
  60. 0.0
  61. 20
  62. 0.0
  63. """
  64. # removed tag (code=47, 0.0442352806926743) from Template: pixel size - caused problems in AutoCAD
  65. # default is a solid fil hatch
  66. hatch_subclass = DefSubclass('AcDbHatch', {
  67. 'elevation': DXFAttr(10, xtype=XType.point3d, default=(0.0, 0.0, 0.0)),
  68. 'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),
  69. 'pattern_name': DXFAttr(2, default='SOLID'), # for solid fill
  70. 'solid_fill': DXFAttr(70, default=1), # pattern fill = 0
  71. 'associative': DXFAttr(71, default=0), # associative flag = 0
  72. 'hatch_style': DXFAttr(75, default=const.HATCH_STYLE_OUTERMOST), # 0=normal; 1=outer; 2=ignore
  73. 'pattern_type': DXFAttr(76, default=const.HATCH_TYPE_PREDEFINED), # 0=user; 1=predefined; 2=custom???
  74. 'pattern_angle': DXFAttr(52, default=0.0), # degrees (360deg = circle)
  75. 'pattern_scale': DXFAttr(41, default=1.0),
  76. 'pattern_double': DXFAttr(77, default=0), # 0=not double; 1= double
  77. 'n_seed_points': DXFAttr(98), # number of seed points
  78. })
  79. GRADIENT_CODES = frozenset([450, 451, 452, 453, 460, 461, 462, 463, 470, 421, 63])
  80. PATH_CODES = frozenset([10, 11, 12, 13, 40, 42, 50, 51, 42, 72, 73, 74, 92, 93, 94, 95, 96, 97, 330])
  81. PATTERN_DEFINITION_LINE_CODES = frozenset((53, 43, 44, 45, 46, 79, 49))
  82. class Hatch(ModernGraphicEntity):
  83. __slots__ = ()
  84. TEMPLATE = ExtendedTags.from_text(_HATCH_TPL)
  85. DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, hatch_subclass)
  86. @property
  87. def AcDbHatch(self) -> Tags:
  88. return self.tags.subclasses[2]
  89. @property
  90. def has_solid_fill(self) -> bool:
  91. return bool(self.dxf.solid_fill)
  92. @property
  93. def has_pattern_fill(self) -> bool:
  94. return not bool(self.dxf.solid_fill)
  95. @property
  96. def has_gradient_data(self) -> bool:
  97. return bool(self.AcDbHatch.get_first_value(450, 0))
  98. @property
  99. def bgcolor(self) -> Optional['RGB']:
  100. try:
  101. xdata_bgcolor = self.tags.get_xdata('HATCHBACKGROUNDCOLOR')
  102. except ValueError:
  103. return None
  104. color = xdata_bgcolor.get_first_value(1071, 0)
  105. return int2rgb(color)
  106. @bgcolor.setter
  107. def bgcolor(self, rgb: 'RGB') -> None:
  108. color_value = rgb2int(rgb) | -0b111110000000000000000000000000 # it's magic
  109. try:
  110. xdata_bgcolor = self.tags.get_xdata('HATCHBACKGROUNDCOLOR')
  111. except DXFValueError: # no xdata for background color found
  112. self.tags.xdata.append(Tags([
  113. DXFTag(1001, 'HATCHBACKGROUNDCOLOR'),
  114. DXFTag(1071, color_value),
  115. ]))
  116. else:
  117. xdata_bgcolor.set_first(DXFTag(1071, color_value))
  118. @bgcolor.deleter
  119. def bgcolor(self) -> None:
  120. try:
  121. xdata_bgcolor = self.tags.get_xdata('HATCHBACKGROUNDCOLOR')
  122. except DXFValueError: # background color does not exist
  123. return
  124. else:
  125. self.tags.xdata.remove(xdata_bgcolor)
  126. @contextmanager
  127. def edit_boundary(self) -> 'BoundaryPathData':
  128. boundary_path_data = BoundaryPathData(self)
  129. yield boundary_path_data
  130. self._set_boundary_path_data(boundary_path_data)
  131. def _set_boundary_path_data(self, boundary_path_data: 'BoundaryPathData') -> None:
  132. # replace existing path tags by the new path
  133. start_index = boundary_path_data.start_index
  134. end_index = boundary_path_data.end_index
  135. self.AcDbHatch[start_index: end_index] = boundary_path_data.dxftags()
  136. def set_solid_fill(self, color: int = 7, style: int = 1, rgb: 'RGB' = None):
  137. self._remove_gradient_data()
  138. if self.has_pattern_fill:
  139. self._remove_pattern_data()
  140. self.dxf.solid_fill = 1
  141. self.dxf.color = color # if a rgb value is present, the color value is ignored by AutoCAD
  142. self.dxf.hatch_style = style
  143. self.dxf.pattern_name = 'SOLID'
  144. self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
  145. if rgb is not None: # if a rgb value is present, the color value is ignored by AutoCAD
  146. self.rgb = rgb # rgb should be a (r, g, b) tuple
  147. def _remove_pattern_data(self) -> None:
  148. if self.has_pattern_fill:
  149. with self.edit_pattern() as e: # delete existing pattern definition
  150. e.clear()
  151. self.AcDbHatch.remove_tags((52, 41, 77))
  152. # Important: AutoCAD does not allow the tags pattern_angle (52), pattern_scale (41), pattern_double (77) for
  153. # hatches with SOLID fill.
  154. def set_gradient(self,
  155. color1: 'RGB' = (0, 0, 0),
  156. color2: 'RGB' = (255, 255, 255),
  157. rotation: float = 0.,
  158. centered: float = 0.,
  159. one_color: int = 0,
  160. tint: float = 0.,
  161. name: str = 'LINEAR') -> None:
  162. if self.drawing is not None and self.drawing.dxfversion < 'AC1018':
  163. raise DXFVersionError(
  164. "Gradient support requires at least DXF version AC1018, this drawing is:%s" % self.drawing.dxfversion)
  165. gradient_data = GradientData()
  166. gradient_data.color1 = color1
  167. gradient_data.color2 = color2
  168. gradient_data.one_color = one_color
  169. gradient_data.rotation = rotation
  170. gradient_data.centered = centered
  171. gradient_data.tint = tint
  172. gradient_data.name = name
  173. self._set_gradient(gradient_data)
  174. def get_gradient(self) -> 'GradientData':
  175. if not self.has_gradient_data:
  176. raise DXFValueError('HATCH has no gradient data.')
  177. return GradientData.from_tags(self.AcDbHatch)
  178. def _set_gradient(self, gradient_data: 'GradientData') -> None:
  179. gradient_data.name = gradient_data.name.upper()
  180. if gradient_data.name not in const.GRADIENT_TYPES:
  181. raise DXFValueError('Invalid gradient type name: %s' % gradient_data.name)
  182. self._remove_pattern_data()
  183. self._remove_gradient_data()
  184. self.dxf.solid_fill = 1
  185. self.dxf.pattern_name = 'SOLID'
  186. self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
  187. self.AcDbHatch.extend(gradient_data.dxftags())
  188. def _remove_gradient_data(self) -> None:
  189. self.AcDbHatch.remove_tags(GRADIENT_CODES)
  190. @contextmanager
  191. def edit_gradient(self) -> 'GradientData':
  192. if not self.has_gradient_data:
  193. raise DXFValueError('HATCH has no gradient data.')
  194. gradient_data = GradientData.from_tags(self.AcDbHatch)
  195. yield gradient_data
  196. self._set_gradient(gradient_data)
  197. def set_pattern_fill(self, name: str, color: int = 7, angle: float = 0., scale: float = 1., double: int = 0,
  198. style: int = 1, pattern_type: int = 1, definition=None) -> None:
  199. self._remove_gradient_data()
  200. self.dxf.solid_fill = 0
  201. self.dxf.pattern_name = name
  202. self.dxf.color = color
  203. self.dxf.hatch_style = style
  204. self.dxf.pattern_type = pattern_type
  205. # safe version of adding pattern fill specific DXF tags:
  206. self.AcDbHatch.remove_tags((52, 41, 77)) # remove pattern angle, pattern scale & pattern double flag if exists
  207. try:
  208. index = self.AcDbHatch.tag_index(76) + 1 # find position of pattern type (76); + 1: insert after tag 76
  209. except DXFValueError:
  210. raise DXFStructureError("HATCH: Missing required DXF tag 'Hatch pattern type' (code=76).")
  211. # insert pattern angle, pattern scale & pattern double flag behind pattern type
  212. self.AcDbHatch[index:index] = [DXFTag(52, angle), DXFTag(41, scale), DXFTag(77, double)]
  213. # place pattern definition right behind pattern double flag (77)
  214. if definition is None:
  215. # get pattern definition from acad standard pattern, default is 'ANSI31'
  216. definition = PATTERN.get(name, PATTERN['ANSI31'])
  217. self.set_pattern_definition(definition)
  218. @contextmanager
  219. def edit_pattern(self) -> 'PatternData':
  220. if self.has_solid_fill:
  221. raise DXFValueError('Solid fill HATCH has no pattern data.')
  222. pattern_data = PatternData(self)
  223. yield pattern_data
  224. self._set_pattern_data(pattern_data)
  225. def _set_pattern_data(self, new_pattern_data: 'PatternData') -> None:
  226. start_index = new_pattern_data.existing_pattern_start_index
  227. end_index = new_pattern_data.existing_pattern_end_index
  228. pattern_tags = new_pattern_data.dxftags()
  229. if start_index == 0: # no existing pattern data
  230. try:
  231. start_index = self.AcDbHatch.tag_index(77) + 1 # hatch pattern double flag, used as insertion point
  232. end_index = start_index
  233. except DXFValueError:
  234. raise DXFStructureError("HATCH: Missing required DXF tag 'Hatch pattern double flag' (code=77).")
  235. # replace existing pattern data
  236. self.AcDbHatch[start_index: end_index] = pattern_tags
  237. def set_pattern_definition(self, lines: Sequence) -> None:
  238. """
  239. Setup hatch patten definition by a list of definition lines and a definition line is a 4-tuple [angle,
  240. base_point, offset, dash_length_items]
  241. - angle: line angle in degrees
  242. - base-point: 2-tuple (x, y)
  243. - offset: 2-tuple (dx, dy)
  244. - dash_length_items: list of dash items (item > 0 is a line, item < 0 is a gap and item == 0.0 is a point)
  245. :param lines: list of definition lines
  246. """
  247. pattern_lines = [PatternDefinitionLine(line[0], line[1], line[2], line[3]) for line in lines]
  248. with self.edit_pattern() as pattern_editor:
  249. pattern_editor.lines = pattern_lines
  250. def get_seed_points(self) -> List[Tuple[float, float]]:
  251. hatch_tags = self.AcDbHatch
  252. first_seed_point_index = self._get_seed_point_index(hatch_tags)
  253. seed_points = hatch_tags.collect_consecutive_tags([10], start=first_seed_point_index)
  254. return [tag.value for tag in seed_points]
  255. def _get_seed_point_index(self, hatch_tags: Tags) -> int:
  256. try:
  257. seed_count_index = hatch_tags.tag_index(98) # find index of 'Number of seed points'
  258. except DXFValueError:
  259. raise DXFStructureError("HATCH: Missing required DXF tag 'Number of seed points' (code=98).")
  260. try:
  261. first_seed_point_index = hatch_tags.tag_index(10, seed_count_index + 1)
  262. except DXFValueError:
  263. raise DXFStructureError("HATCH: Missing required DXF tags 'seed point X value' (code=10).")
  264. return first_seed_point_index
  265. def set_seed_points(self, points: Sequence[Tuple[float, float]]) -> None:
  266. if len(points) < 1:
  267. raise DXFValueError("Param points should be a collection of 2D points and requires at least one point.")
  268. hatch_tags = self.AcDbHatch
  269. first_seed_point_index = self._get_seed_point_index(hatch_tags)
  270. existing_seed_points = hatch_tags.collect_consecutive_tags([10],
  271. start=first_seed_point_index) # don't rely on 'Number of seed points'
  272. new_seed_points = [DXFVertex(10, (point[0], point[1])) for point in points] # only use x and y coordinate,
  273. self.dxf.n_seed_points = len(new_seed_points) # set new count of seed points
  274. # replace existing seed points
  275. hatch_tags[first_seed_point_index: first_seed_point_index + len(existing_seed_points)] = new_seed_points
  276. class BoundaryPathData:
  277. def __init__(self, hatch):
  278. self.start_index = 0
  279. self.end_index = 0
  280. self.paths = self._setup_paths(hatch.AcDbHatch)
  281. def _setup_paths(self, tags: Tags) -> List['PolylinePath']:
  282. paths = []
  283. try:
  284. self.start_index = tags.tag_index(91) # code 91=Number of boundary paths (loops)
  285. n_paths = tags[self.start_index].value
  286. except DXFValueError:
  287. raise DXFStructureError("HATCH: Missing required DXF tag 'Number of boundary paths (loops)' (code=91).")
  288. self.end_index = self.start_index + 1 # + 1 for Tag(91, Number of boundary paths)
  289. if n_paths == 0: # created by ezdxf from template without path data
  290. return paths
  291. all_path_tags = tags.collect_consecutive_tags(PATH_CODES, start=self.start_index + 1)
  292. self.end_index = self.start_index + len(all_path_tags) + 1 # + 1 for Tag(91, Number of boundary paths)
  293. # end_index: stored for Hatch._set_boundary_path_data()
  294. grouped_path_tags = group_tags(all_path_tags, splitcode=92)
  295. for path_tags in grouped_path_tags:
  296. path_type_flags = path_tags[0].value
  297. is_polyline_path = bool(path_type_flags & 2)
  298. path = PolylinePath.from_tags(path_tags) if is_polyline_path else EdgePath.from_tags(path_tags)
  299. path.path_type_flags = path_type_flags
  300. paths.append(path)
  301. return paths
  302. def clear(self) -> None:
  303. self.paths = []
  304. def add_polyline_path(self, path_vertices: Sequence[Tuple[float, float]], is_closed: int = 1,
  305. flags: int = 1) -> 'PolylinePath':
  306. new_path = PolylinePath()
  307. new_path.set_vertices(path_vertices, is_closed)
  308. new_path.path_type_flags = flags | const.BOUNDARY_PATH_POLYLINE
  309. self.paths.append(new_path)
  310. return new_path
  311. def add_edge_path(self, flags: int = 1) -> 'EdgePath':
  312. new_path = EdgePath()
  313. new_path.path_type_flags = flags
  314. self.paths.append(new_path)
  315. return new_path
  316. def dxftags(self) -> List[DXFTag]:
  317. tags = [DXFTag(91, len(self.paths))]
  318. for path in self.paths:
  319. tags.extend(path.dxftags())
  320. return tags
  321. def pop_source_boundary_objects_tags(all_path_tags: Tags) -> List[DXFTag]:
  322. source_boundary_object_tags = []
  323. while len(all_path_tags):
  324. if all_path_tags[-1].code in (97, 333):
  325. last_tag = all_path_tags.pop()
  326. if last_tag.code == 330:
  327. source_boundary_object_tags.append(last_tag)
  328. else: # code == 97
  329. # result list does not contain the length tag!
  330. source_boundary_object_tags.reverse()
  331. return source_boundary_object_tags
  332. else:
  333. return [] # no source boundary objects found - entity is not valid for AutoCAD
  334. def build_source_boundary_object_tags(source_boundary_objects: Sequence[DXFTag]) -> List[DXFTag]:
  335. source_boundary_object_tags = [DXFTag(97, len(source_boundary_objects))]
  336. source_boundary_object_tags.extend(source_boundary_objects)
  337. return source_boundary_object_tags
  338. class PolylinePath:
  339. PATH_TYPE = 'PolylinePath'
  340. def __init__(self):
  341. self.path_type_flags = const.BOUNDARY_PATH_POLYLINE
  342. self.is_closed = 0
  343. self.vertices = [] # type: List[Tuple[float, float, float]] # list of 2D coordinates with bulge values (x, y, bulge); bulge default = 0.0
  344. self.source_boundary_objects = [] # type: List[DXFTag] # (330, handle) tags
  345. @staticmethod
  346. def from_tags(tags) -> 'PolylinePath':
  347. polyline_path = PolylinePath()
  348. polyline_path._setup_path(tags)
  349. return polyline_path
  350. def _setup_path(self, tags: Tags) -> None:
  351. self.source_boundary_objects = pop_source_boundary_objects_tags(tags)
  352. for tag in tags:
  353. code, value = tag
  354. if code == 10: # vertex coordinates
  355. self.vertices.append((value[0], value[1], 0.0)) # (x, y, bulge); bulge default = 0.0
  356. elif code == 42: # bulge value
  357. x, y, bulge = self.vertices.pop() # last value
  358. self.vertices.append((x, y, value)) # last coordinates with new bulge value
  359. elif code == 72:
  360. pass # ignore this value
  361. elif code == 73:
  362. self.is_closed = value
  363. elif code == 92:
  364. self.path_type_flags = value
  365. elif code == 93: # number of polyline vertices
  366. pass # ignore this value
  367. def set_vertices(self, vertices: Sequence[Sequence[float]], is_closed: bool = 1) -> None:
  368. new_vertices = []
  369. for vertex in vertices:
  370. if len(vertex) == 2:
  371. x, y = vertex
  372. bulge = 0
  373. elif len(vertex) == 3:
  374. x, y, bulge = vertex
  375. else:
  376. raise DXFValueError("Invalid vertex format, expected (x, y) or (x, y, bulge)")
  377. new_vertices.append((x, y, bulge))
  378. self.vertices = new_vertices
  379. self.is_closed = is_closed
  380. def clear(self) -> None:
  381. self.vertices = []
  382. self.is_closed = 0
  383. self.source_boundary_objects = []
  384. def has_bulge(self) -> bool:
  385. for x, y, bulge in self.vertices:
  386. if bulge != 0:
  387. return True
  388. return False
  389. def dxftags(self) -> List[DXFTag]:
  390. has_bulge = self.has_bulge()
  391. vtags = []
  392. for x, y, bulge in self.vertices:
  393. vtags.append(DXFVertex(10, (float(x), float(y))))
  394. if has_bulge:
  395. vtags.append(DXFTag(42, float(bulge)))
  396. tags = [
  397. DXFTag(92, int(self.path_type_flags)),
  398. DXFTag(72, int(has_bulge)),
  399. DXFTag(73, int(self.is_closed)),
  400. DXFTag(93, len(self.vertices)),
  401. ]
  402. tags.extend(vtags)
  403. tags.extend(build_source_boundary_object_tags(self.source_boundary_objects))
  404. return tags
  405. class EdgePath:
  406. PATH_TYPE = 'EdgePath'
  407. def __init__(self):
  408. self.path_type_flags = const.BOUNDARY_PATH_DEFAULT
  409. self.edges = []
  410. self.source_boundary_objects = []
  411. @staticmethod
  412. def from_tags(tags: Tags) -> 'EdgePath':
  413. edge_path = EdgePath()
  414. edge_path._setup_path(tags)
  415. return edge_path
  416. def _setup_path(self, tags: Tags) -> None:
  417. self.source_boundary_objects = pop_source_boundary_objects_tags(tags)
  418. edge_groups = group_tags(tags, splitcode=72)
  419. for edge_tags in edge_groups:
  420. self.edges.append(self._setup_edge(edge_tags))
  421. def _setup_edge(self, tags: Tags) -> 'EdgeTypes':
  422. edge_type = tags[0].value
  423. if 0 < edge_type < 5:
  424. return EDGE_CLASSES[edge_type].from_tags(tags[1:])
  425. else:
  426. raise DXFStructureError("HATCH: unknown edge type: {}".format(edge_type))
  427. def add_line(self, start: int, end: int) -> 'LineEdge':
  428. line = LineEdge()
  429. line.start = start
  430. line.end = end
  431. self.edges.append(line)
  432. return line
  433. def add_arc(self, center: Tuple[float, float],
  434. radius: float = 1.,
  435. start_angle: float = 0.,
  436. end_angle: float = 360.,
  437. is_counter_clockwise: int = 0) -> 'ArcEdge':
  438. arc = ArcEdge()
  439. arc.center = center
  440. arc.radius = radius
  441. arc.start_angle = start_angle
  442. arc.end_angle = end_angle
  443. arc.is_counter_clockwise = 1 if bool(is_counter_clockwise) else 0
  444. self.edges.append(arc)
  445. return arc
  446. def add_ellipse(self, center: Tuple[float, float],
  447. major_axis: Tuple[float, float] = (1., 0.),
  448. ratio: float = 1.,
  449. start_angle: float = 0.,
  450. end_angle: float = 360.,
  451. is_counter_clockwise: int = 0) -> 'EllipseEdge':
  452. if ratio > 1.:
  453. raise DXFValueError("Parameter 'ratio' has to be <= 1.0")
  454. ellipse = EllipseEdge()
  455. ellipse.center = center
  456. ellipse.major_axis = major_axis
  457. ellipse.ratio = ratio
  458. ellipse.start_angle = start_angle
  459. ellipse.end_angle = end_angle
  460. ellipse.is_counter_clockwise = is_counter_clockwise
  461. self.edges.append(ellipse)
  462. return ellipse
  463. def add_spline(self, fit_points: Iterable[Tuple[float, float]] = None,
  464. control_points: Iterable[Tuple[float, float]] = None,
  465. knot_values: Iterable[float] = None,
  466. weights: Iterable[float] = None,
  467. degree: int = 3,
  468. rational: int = 0,
  469. periodic: int = 0) -> 'SplineEdge':
  470. spline = SplineEdge()
  471. if fit_points is not None:
  472. spline.fit_points = list(fit_points)
  473. if control_points is not None:
  474. spline.control_points = list(control_points)
  475. if knot_values is not None:
  476. spline.knot_values = list(knot_values)
  477. if weights is not None:
  478. spline.weights = list(weights)
  479. spline.degree = degree
  480. spline.rational = int(rational)
  481. spline.periodic = int(periodic)
  482. self.edges.append(spline)
  483. return spline
  484. def add_spline_control_frame(self, fit_points: Iterable[Tuple[float, float]],
  485. degree: int = 3,
  486. method: str = 'distance',
  487. power: float = .5) -> 'SplineEdge':
  488. bspline = bspline_control_frame(fit_points=fit_points, degree=degree, method=method, power=power)
  489. return self.add_spline(
  490. fit_points=fit_points,
  491. control_points=bspline.control_points,
  492. knot_values=bspline.knot_values(),
  493. )
  494. def clear(self) -> None:
  495. self.edges = []
  496. def dxftags(self) -> List[DXFTag]:
  497. tags = [DXFTag(92, int(self.path_type_flags)), DXFTag(93, len(self.edges))]
  498. for edge in self.edges:
  499. tags.extend(edge.dxftags())
  500. tags.extend(build_source_boundary_object_tags(self.source_boundary_objects))
  501. return tags
  502. class LineEdge:
  503. EDGE_TYPE = "LineEdge"
  504. # more struct than object
  505. def __init__(self):
  506. self.start = (0, 0) # type: Tuple[float, float]
  507. self.end = (0, 0) # type: Tuple[float, float]
  508. @staticmethod
  509. def from_tags(tags: Tags) -> 'LineEdge':
  510. edge = LineEdge()
  511. for tag in tags:
  512. code, value = tag
  513. if code == 10:
  514. edge.start = value
  515. elif code == 11:
  516. edge.end = value
  517. return edge
  518. def dxftags(self) -> List[DXFTag]:
  519. return [
  520. DXFTag(72, 1), # edge type
  521. DXFVertex(10, self.start),
  522. DXFVertex(11, self.end),
  523. ]
  524. class ArcEdge:
  525. EDGE_TYPE = "ArcEdge"
  526. # more struct than object
  527. def __init__(self):
  528. self.center = (0., 0.) # type: Tuple[float, float]
  529. self.radius = 1.
  530. self.start_angle = 0.
  531. self.end_angle = 360.
  532. self.is_counter_clockwise = 0
  533. @staticmethod
  534. def from_tags(tags: Tags) -> 'ArcEdge':
  535. edge = ArcEdge()
  536. for tag in tags:
  537. code, value = tag
  538. if code == 10:
  539. edge.center = value
  540. elif code == 40:
  541. edge.radius = value
  542. elif code == 50:
  543. edge.start_angle = value
  544. elif code == 51:
  545. edge.end_angle = value
  546. elif code == 73:
  547. edge.is_counter_clockwise = value
  548. return edge
  549. def dxftags(self) -> List[DXFTag]:
  550. return [
  551. DXFTag(72, 2), # edge type
  552. DXFVertex(10, self.center),
  553. DXFTag(40, self.radius),
  554. DXFTag(50, self.start_angle),
  555. DXFTag(51, self.end_angle),
  556. DXFTag(73, self.is_counter_clockwise),
  557. ]
  558. class EllipseEdge:
  559. EDGE_TYPE = "EllipseEdge"
  560. # more struct than object
  561. def __init__(self):
  562. self.center = (0., 0.) # type: Tuple[float, float]
  563. self.major_axis = (1., 0.) # type: Tuple[float, float] # Endpoint of major axis relative to center point (in OCS)
  564. self.ratio = 1.
  565. self.start_angle = 0.
  566. self.end_angle = 360.
  567. self.is_counter_clockwise = 0
  568. @staticmethod
  569. def from_tags(tags: Tags) -> 'EllipseEdge':
  570. edge = EllipseEdge()
  571. for tag in tags:
  572. code, value = tag
  573. if code == 10:
  574. edge.center = value
  575. elif code == 11:
  576. edge.major_axis = value
  577. elif code == 40:
  578. edge.ratio = value
  579. elif code == 50:
  580. edge.start_angle = value
  581. elif code == 51:
  582. edge.end_angle = value
  583. elif code == 73:
  584. edge.is_counter_clockwise = value
  585. return edge
  586. def dxftags(self) -> List[DXFTag]:
  587. return [
  588. DXFTag(72, 3), # edge type
  589. DXFVertex(10, self.center),
  590. DXFVertex(11, self.major_axis),
  591. DXFTag(40, self.ratio),
  592. DXFTag(50, self.start_angle),
  593. DXFTag(51, self.end_angle),
  594. DXFTag(73, self.is_counter_clockwise)
  595. ]
  596. class SplineEdge:
  597. EDGE_TYPE = "SplineEdge"
  598. def __init__(self):
  599. self.degree = 3 # code = 94
  600. self.rational = 0 # code = 73
  601. self.periodic = 0 # code = 74
  602. self.knot_values = [] # type: List[float]
  603. self.control_points = [] # type: List[Tuple[float, float]]
  604. self.fit_points = [] # type: List[Tuple[float, float]]
  605. self.weights = [] # type: List[float]
  606. self.start_tangent = (0, 0) # type: Tuple[float, float]
  607. self.end_tangent = (0, 0) # type: Tuple[float, float]
  608. @staticmethod
  609. def from_tags(tags: Tags) -> 'SplineEdge':
  610. edge = SplineEdge()
  611. for tag in tags:
  612. code, value = tag
  613. if code == 94:
  614. edge.degree = value
  615. elif code == 73:
  616. edge.rational = value
  617. elif code == 74:
  618. edge.periodic = value
  619. elif code == 40:
  620. edge.knot_values.append(value)
  621. elif code == 42:
  622. edge.weights.append(value)
  623. elif code == 10:
  624. edge.control_points.append(value)
  625. elif code == 11:
  626. edge.fit_points.append(value)
  627. elif code == 12:
  628. edge.start_tangent = value
  629. elif code == 13:
  630. edge.end_tangent = value
  631. return edge
  632. def dxftags(self) -> List[DXFTag]:
  633. tags = [
  634. DXFTag(72, 4), # edge type
  635. DXFTag(94, int(self.degree)),
  636. DXFTag(73, int(self.rational)),
  637. DXFTag(74, int(self.periodic)),
  638. DXFTag(95, len(self.knot_values)), # number of knots
  639. DXFTag(96, len(self.control_points)), # number of control points
  640. ]
  641. # build knot values list
  642. # knot values have to be present and valid, otherwise AutoCAD crashes
  643. if len(self.knot_values):
  644. tags.extend([DXFTag(40, float(value)) for value in self.knot_values])
  645. else:
  646. raise DXFValueError("SplineEdge: missing required knot values")
  647. # build control points
  648. # control points have to be present and valid, otherwise AutoCAD crashes
  649. tags.extend([DXFVertex(10, (float(value[0]), float(value[1]))) for value in self.control_points])
  650. # build weights list, optional
  651. tags.extend([DXFTag(42, float(value)) for value in self.weights])
  652. # build fit points
  653. # fit points have to be present and valid, otherwise AutoCAD crashes
  654. # edit 2016-12-20: this is not true - there are cases with no fit points and without crashing AutoCAD
  655. tags.append(DXFTag(97, len(self.fit_points)))
  656. tags.extend([DXFVertex(11, (float(value[0]), float(value[1]))) for value in self.fit_points])
  657. tags.append(DXFVertex(12, (float(self.start_tangent[0]), float(self.start_tangent[1]))))
  658. tags.append(DXFVertex(13, (float(self.end_tangent[0]), float(self.end_tangent[1]))))
  659. return tags
  660. EDGE_CLASSES = [None, LineEdge, ArcEdge, EllipseEdge, SplineEdge]
  661. EdgeTypes = Union[LineEdge, ArcEdge, EllipseEdge, SplineEdge]
  662. class PatternData:
  663. def __init__(self, hatch: Hatch):
  664. self.existing_pattern_start_index = 0
  665. self.existing_pattern_end_index = 0
  666. self.lines = self._setup_pattern_lines(hatch.AcDbHatch)
  667. def _setup_pattern_lines(self, tags: Tags) -> List['PatternDefinitionLine']:
  668. try:
  669. self.existing_pattern_start_index = tags.tag_index(78) # code 78=Number of patter definition lines
  670. except DXFValueError: # no pattern definition lines found
  671. self.existing_pattern_start_index = 0
  672. self.existing_pattern_end_index = 0
  673. return []
  674. all_pattern_tags = tags.collect_consecutive_tags(PATTERN_DEFINITION_LINE_CODES,
  675. start=self.existing_pattern_start_index + 1)
  676. self.existing_pattern_end_index = self.existing_pattern_start_index + len(
  677. all_pattern_tags) + 1 # + 1 for Tag(78, Number of boundary paths)
  678. # existing_pattern_end_index: stored for Hatch._set_pattern_data()
  679. grouped_line_tags = group_tags(all_pattern_tags, splitcode=53)
  680. return [PatternDefinitionLine.from_tags(line_tags) for line_tags in grouped_line_tags]
  681. def clear(self) -> None:
  682. self.lines = []
  683. def add_line(self,
  684. angle: float = 0.,
  685. base_point: Tuple[float, float] = (0., 0.),
  686. offset: Tuple[float, float] = (0., 0.),
  687. dash_length_items: List[float] = None) -> None:
  688. self.lines.append(self.new_line(angle, base_point, offset, dash_length_items))
  689. @staticmethod
  690. def new_line(angle: float = 0.,
  691. base_point: Tuple[float, float] = (0., 0.),
  692. offset: Tuple[float, float] = (0., 0.),
  693. dash_length_items: List[float] = None) -> 'PatternDefinitionLine':
  694. if dash_length_items is None:
  695. raise DXFValueError("Parameter 'dash_length_items' must not be None.")
  696. return PatternDefinitionLine(angle, base_point, offset, dash_length_items)
  697. def dxftags(self) -> List[DXFTag]:
  698. if len(self.lines):
  699. tags = [DXFTag(78, len(self.lines))]
  700. for line in self.lines:
  701. tags.extend(line.dxftags())
  702. else:
  703. tags = []
  704. return tags
  705. def __str__(self) -> str:
  706. return "[" + ",".join(str(line) for line in self.lines) + "]"
  707. class PatternDefinitionLine:
  708. def __init__(self,
  709. angle: float = 0.,
  710. base_point: Tuple[float, float] = (0., 0.),
  711. offset: Tuple[float, float] = (0., 0.),
  712. dash_length_items: List[float] = None):
  713. self.angle = angle # as always in degrees (circle = 360 deg)
  714. self.base_point = base_point
  715. self.offset = offset
  716. self.dash_length_items = [] if dash_length_items is None else dash_length_items # type: List[float]
  717. # dash_length_items = [item0, item1, ...]
  718. # item > 0 is line, < 0 is gap, 0.0 = dot;
  719. @staticmethod
  720. def from_tags(tags: Tags) -> 'PatternDefinitionLine':
  721. p = {53: 0, 43: 0, 44: 0, 45: 0, 46: 0}
  722. dash_length_items = []
  723. for tag in tags:
  724. code, value = tag
  725. if code == 49:
  726. dash_length_items.append(value)
  727. else:
  728. p[code] = value
  729. return PatternDefinitionLine(p[53], (p[43], p[44]), (p[45], p[46]), dash_length_items)
  730. def dxftags(self) -> List[DXFTag]:
  731. tags = [
  732. DXFTag(53, self.angle),
  733. DXFTag(43, self.base_point[0]),
  734. DXFTag(44, self.base_point[1]),
  735. DXFTag(45, self.offset[0]),
  736. DXFTag(46, self.offset[1]),
  737. DXFTag(79, len(self.dash_length_items)),
  738. ]
  739. tags.extend(DXFTag(49, item) for item in self.dash_length_items)
  740. return tags
  741. def __str__(self):
  742. return "[{0.angle}, {0.base_point}, {0.offset}, {0.dash_length_items}]".format(self)
  743. class GradientData:
  744. def __init__(self):
  745. self.color1 = (0, 0, 0) # type: RGB
  746. self.color2 = (255, 255, 255) # type: RGB
  747. self.one_color = 0 # if 1 - a fill that uses a smooth transition between color1 and a specified tint
  748. self.rotation = 0. # use grad NOT radians here, because there should be ONE system for all angles
  749. self.centered = 0.
  750. self.tint = 0.0
  751. self.name = 'LINEAR'
  752. @staticmethod
  753. def from_tags(tags: Tags) -> 'GradientData':
  754. gdata = GradientData()
  755. try:
  756. index = tags.tag_index(450) # required tag if hatch has gradient data
  757. except DXFValueError:
  758. raise DXFStructureError("HATCH: Missing required DXF tag for gradient data (code=450).")
  759. # if tags[index].value == 0 hatch is a solid ????
  760. first_color_value = True
  761. for code, value in tags[index:]:
  762. if code == 460:
  763. gdata.rotation = (value / math.pi) * 180. # radians to grad
  764. elif code == 461:
  765. gdata.centered = value
  766. elif code == 452:
  767. gdata.one_color = value
  768. elif code == 462:
  769. gdata.tint = value
  770. elif code == 470:
  771. gdata.name = value
  772. elif code == 421:
  773. # code == 63 color as ACI, can be ignored
  774. if first_color_value:
  775. gdata.color1 = int2rgb(value)
  776. first_color_value = False
  777. else:
  778. gdata.color2 = int2rgb(value)
  779. return gdata
  780. def dxftags(self) -> List[DXFTag]:
  781. return [
  782. DXFTag(450, 1), # gradient
  783. DXFTag(451, 0), # reserved for the future
  784. DXFTag(452, self.one_color), # one (1) or two (0) color gradient
  785. DXFTag(453, 2), # number of colors
  786. DXFTag(460, (self.rotation / 180.) * math.pi), # rotation angle in radians
  787. DXFTag(461, self.centered), # see DXF standard
  788. DXFTag(462, self.tint), # see DXF standard
  789. DXFTag(463, 0), # first value, see DXF standard
  790. # code == 63 "color as ACI" can be left off
  791. DXFTag(421, rgb2int(self.color1)), # first color
  792. DXFTag(463, 1), # second value, see DXF standard
  793. # code == 63 "color as ACI" can be left off
  794. DXFTag(421, rgb2int(self.color2)), # second color
  795. DXFTag(470, self.name),
  796. ]