layouts.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. # Created: 21.03.2011
  2. # Copyright (C) 2011, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, Union, List, Iterable, Tuple, Dict, Hashable, Sequence, Optional
  5. from ezdxf.graphicsfactory import GraphicsFactory
  6. from ezdxf.entityspace import EntitySpace
  7. from ezdxf.query import EntityQuery
  8. from ezdxf.groupby import groupby
  9. from ezdxf.lldxf.const import STD_SCALES, DXFValueError
  10. if TYPE_CHECKING:
  11. from ezdxf.eztypes import Drawing, EntityDB, LayoutType, DXFEntity, TagWriter, DXFFactoryType, EntitySpace
  12. from ezdxf.eztypes import KeyFunc, GenericLayoutType
  13. class DXF12Layouts:
  14. """
  15. The Layout container.
  16. """
  17. def __init__(self, drawing: 'Drawing'):
  18. entities = drawing.sections.entities
  19. model_space = entities.model_space_entities()
  20. self._modelspace = DXF12Layout(model_space, drawing.dxffactory, 0)
  21. paper_space = entities.active_layout_entities()
  22. self._paperspace = DXF12Layout(paper_space, drawing.dxffactory, 1)
  23. entities.clear() # remove entities for entities section -> stored in layouts
  24. def __iter__(self) -> 'LayoutType':
  25. yield self._modelspace
  26. yield self._paperspace
  27. def __len__(self) -> int:
  28. return 2
  29. def modelspace(self) -> 'LayoutType':
  30. return self._modelspace
  31. def get(self, name: str = "") -> 'LayoutType':
  32. # AC1009 supports only one paperspace/layout
  33. return self._paperspace
  34. def names(self) -> List[str]:
  35. return []
  36. def get_layout_for_entity(self, entity: 'DXFEntity') -> 'LayoutType':
  37. # paperspace attribute defaults to 0 if not present
  38. if entity in self._modelspace:
  39. return self._modelspace
  40. elif entity in self._paperspace:
  41. return self._paperspace
  42. else:
  43. return None
  44. def active_layout(self) -> 'LayoutType':
  45. return self._paperspace
  46. def write_entities_section(self, tagwriter: 'TagWriter') -> None:
  47. self._modelspace.write(tagwriter)
  48. self._paperspace.write(tagwriter)
  49. class BaseLayout(GraphicsFactory):
  50. """
  51. Base class for DXF12Layout() and DXF12BlockLayout()
  52. Entities are wrapped into class GraphicEntity() or inherited.
  53. """
  54. def __init__(self, dxffactory: 'DXFFactoryType', entity_space: 'EnitySpace'):
  55. super(BaseLayout, self).__init__(dxffactory)
  56. self._entity_space = entity_space
  57. def __len__(self) -> int:
  58. """
  59. Entities count.
  60. """
  61. return len(self._entity_space)
  62. def __iter__(self) -> Iterable['DXFEntity']:
  63. """
  64. Iterate over all drawing entities in this layout.
  65. Returns: :class:`DXFEntity`
  66. """
  67. wrap = self._dxffactory.wrap_handle
  68. for handle in self._entity_space:
  69. yield wrap(handle)
  70. @property
  71. def entitydb(self) -> 'EnityDB':
  72. return self._dxffactory.entitydb
  73. @property
  74. def drawing(self) -> 'Drawing':
  75. return self._dxffactory.drawing
  76. def build_and_add_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
  77. """
  78. Create entity in drawing database and add entity to the entity space.
  79. Args:
  80. type_ (str): DXF type string, like 'LINE', 'CIRCLE' or 'LWPOLYLINE'
  81. dxfattribs (dict): DXF attributes for the new entity
  82. """
  83. entity = self.build_entity(type_, dxfattribs)
  84. self.add_entity(entity)
  85. return entity
  86. def build_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
  87. """
  88. Create entity in drawing database, returns a wrapper class inherited from GraphicEntity().
  89. Adds entity to the drawing database.
  90. Args:
  91. type_ (str): DXF type string, like 'LINE', 'CIRCLE' or 'LWPOLYLINE'
  92. dxfattribs(dict): DXF attributes for the new entity
  93. """
  94. entity = self._dxffactory.create_db_entry(type_, dxfattribs)
  95. self._set_paperspace(entity)
  96. return entity
  97. def add_entity(self, entity: 'DXFEntity') -> None:
  98. """
  99. Add an existing :class:`DXFEntity` to a layout, but be sure to unlink (:meth:`~Layout.unlink_entity()`) first the entity
  100. from the previous owner layout.
  101. """
  102. self._entity_space.append(entity.dxf.handle)
  103. self._set_paperspace(entity)
  104. for linked_entity in entity.linked_entities():
  105. self._set_paperspace(linked_entity)
  106. def unlink_entity(self, entity: 'DXFEntity') -> None:
  107. """
  108. Unlink `entity` from layout but does not delete entity from the drawing database.
  109. Removes `entity` just from entity space but not from the drawing database.
  110. Args:
  111. entity: :class:`DXFEntity`
  112. """
  113. self._entity_space.delete_entity(entity)
  114. entity.dxf.paperspace = -1 # set invalid paper space
  115. if entity.supports_dxf_attrib('owner'): # R2000
  116. entity.dxf.owner = '0'
  117. def delete_entity(self, entity: 'DXFEntity') -> None:
  118. """
  119. Delete `entity` from layout (entity space) and drawing database.
  120. Args:
  121. entity: :class:`DXFEntity`
  122. """
  123. self.entitydb.delete_entity(entity) # 1. delete from drawing database
  124. self.unlink_entity(entity) # 2. unlink from entity space
  125. def delete_all_entities(self) -> None:
  126. """
  127. Delete all entities from Layout (entity space) and from drawing database.
  128. """
  129. # noinspection PyTypeChecker
  130. for entity in list(self): # temp list, because delete modifies the base data structure of the iterator
  131. self.delete_entity(entity)
  132. def _set_paperspace(self, entity: 'DXFEntity') -> None:
  133. pass # only for DXF 2000 and later necessary
  134. def get_entity_by_handle(self, handle: str) -> 'DXFEntity':
  135. """
  136. Get entity by handle as GraphicEntity() or inherited.
  137. """
  138. return self._dxffactory.wrap_handle(handle)
  139. def query(self, query='*') -> EntityQuery:
  140. """
  141. Get all DXF entities matching the :ref:`entity query string`.
  142. Args:
  143. query: eintity query string
  144. Returns: :class:`EntityQuery`
  145. """
  146. return EntityQuery(iter(self), query)
  147. def groupby(self, dxfattrib: str = "", key: 'KeyFunc' = None) -> Dict[Hashable, List['DXFEntity']]:
  148. """
  149. Returns a dict of entity lists, where entities are grouped by a `dxfattrib` or a `key` function.
  150. Args:
  151. dxfattrib: grouping by DXF attribute like "layer"
  152. key: key function, which accepts a :class:`DXFEntity` as argument, returns grouping key of this entity or
  153. None to ignore this object. Reason for ignoring: a queried DXF attribute is not supported by this
  154. entity.
  155. """
  156. return groupby(iter(self), dxfattrib, key)
  157. def move_to_layout(self, entity: 'DXFEntity', layout: 'GenericLayoutType') -> None:
  158. """
  159. Move entity from block layout to another layout.
  160. Args:
  161. entity: DXF entity to move
  162. layout: any layout (model space, paper space, block)
  163. """
  164. if entity.dxf.handle in self._entity_space:
  165. self.unlink_entity(entity)
  166. layout.add_entity(entity)
  167. else:
  168. raise DXFValueError('Layout does not contain entity.')
  169. class DXF12Layout(BaseLayout):
  170. """
  171. Layout representation
  172. """
  173. def __init__(self, entityspace: 'EntitySpace', dxffactory: 'DXFFactoryType', paperspace: int = 0):
  174. super(DXF12Layout, self).__init__(dxffactory, entityspace)
  175. self._paperspace = paperspace
  176. # start of public interface
  177. def __contains__(self, entity: Union[str, 'DXFEntity']):
  178. """
  179. Test if the layout contains the drawing element `entity` (aka `in` operator).
  180. """
  181. if not hasattr(entity, 'dxf'): # entity is a handle and not a wrapper class
  182. entity = self.get_entity_by_handle(entity)
  183. return entity.dxf.paperspace == self._paperspace and entity.dxf.handle in self._entity_space
  184. # end of public interface
  185. def _set_paperspace(self, entity: 'DXFEntity'):
  186. entity.dxf.paperspace = self._paperspace
  187. def page_setup(self, size: Tuple[int, int] = (297, 210),
  188. margins: Tuple[int, int, int, int] = (0, 0, 0, 0),
  189. units: str = 'mm',
  190. offset: Tuple[int, int] = (0, 0),
  191. rotation: float = 0,
  192. scale: int = 16) -> None:
  193. if self._paperspace == 0:
  194. raise DXFTypeError("No paper setup for model space.")
  195. # remove existing viewports
  196. for viewport in self.viewports():
  197. self.delete_entity(viewport)
  198. if int(rotation) not in (0, 1, 2, 3):
  199. raise DXFValueError("valid rotation values: 0-3")
  200. if isinstance(scale, int):
  201. scale = STD_SCALES.get(scale, (1, 1))
  202. if scale[0] == 0:
  203. raise DXFValueError("scale numerator can't be 0.")
  204. if scale[1] == 0:
  205. raise DXFValueError("scale denominator can't be 0.")
  206. scale_factor = scale[1] / scale[0]
  207. # TODO: don't know how to set inch or mm mode in R12
  208. units = units.lower()
  209. if units.startswith('inch'):
  210. units = 'Inches'
  211. plot_paper_units = 0
  212. unit_factor = 25.4 # inch to mm
  213. elif units == 'mm':
  214. units = 'MM'
  215. plot_paper_units = 1
  216. unit_factor = 1.0
  217. else:
  218. raise DXFValueError('Supported units: "mm" and "inch"')
  219. # all viewport parameters are scaled paper space units
  220. def paper_units(value):
  221. return value * scale_factor
  222. # TODO: don't know how paper setup in DXF R12 works
  223. paper_width, paper_height = size
  224. # TODO: don't know how margins setup in DXF R12 works
  225. margin_top, margin_right, margin_bottom, margin_left = margins
  226. paper_width = paper_units(size[0])
  227. paper_height = paper_units(size[1])
  228. plimmin = self.drawing.header['$PLIMMIN'] = (0, 0)
  229. plimmax = self.drawing.header['$PLIMMAX'] = (paper_width, paper_height)
  230. # TODO: don't know how paper setup in DXF R12 works
  231. pextmin = self.drawing.header['$PEXTMIN'] = (0, 0, 0)
  232. pextmax = self.drawing.header['$PEXTMAX'] = (paper_width, paper_height, 0)
  233. # printing area
  234. printable_width = paper_width - paper_units(margin_left) - paper_units(margin_right)
  235. printable_height = paper_height - paper_units(margin_bottom) - paper_units(margin_top)
  236. # AutoCAD viewport (window) size
  237. vp_width = paper_width * 1.1
  238. vp_height = paper_height * 1.1
  239. # center of printing area
  240. center = (printable_width / 2, printable_height / 2)
  241. # create 'main' viewport
  242. main_viewport = self.add_viewport(
  243. center=center, # no influence to 'main' viewport?
  244. size=(vp_width, vp_height), # I don't get it, just use paper size!
  245. view_center_point=center, # same as center
  246. view_height=vp_height, # view height in paper space units
  247. )
  248. main_viewport.dxf.id = 1 # set as main viewport
  249. main_viewport.dxf.status = 2 # AutoCAD default value
  250. with main_viewport.edit_data() as vpdata:
  251. vpdata.view_mode = 1000 # AutoDesk default
  252. def get_paper_limits(self) -> Tuple[float, float]:
  253. """
  254. Returns paper limits in plot paper units
  255. """
  256. limmin = self.drawing.header.get('$PLIMMIN', (0, 0))
  257. limmax = self.drawing.header.get('$PLIMMAX', (0, 0))
  258. return limmin, limmax
  259. @property
  260. def layout_key(self) -> int:
  261. return self._paperspace
  262. def viewports(self) -> List['DXFEntity']:
  263. """
  264. Get all VIEWPORT entities defined in the layout. Returns a list of Viewport() objects, sorted by id, the first
  265. entity is always the paper space view with the id=1.
  266. """
  267. vports = [entity for entity in self if entity.dxftype() == 'VIEWPORT']
  268. vports.sort(key=lambda e: e.dxf.id)
  269. return vports
  270. def renumber_viewports(self) -> None:
  271. for num, viewport in enumerate(self.viewports(), start=1):
  272. viewport.dxf.id = num
  273. def write(self, tagwriter: 'TagWriter') -> None:
  274. self._entity_space.write(tagwriter)
  275. def add_viewport(self,
  276. center: Tuple[float, float],
  277. size: Tuple[float, float],
  278. view_center_point: Tuple[float, float],
  279. view_height: float,
  280. dxfattribs: dict = None) -> 'DXFEntity':
  281. if dxfattribs is None:
  282. dxfattribs = {}
  283. else:
  284. dxfattribs = dict(dxfattribs)
  285. width, height = size
  286. attribs = {
  287. 'center': center,
  288. 'width': width,
  289. 'height': height,
  290. 'status': 1, # by default highest priority (stack order)
  291. 'layer': 'VIEWPORTS', # use separated layer to turn off for plotting
  292. }
  293. attribs.update(dxfattribs)
  294. # DXF R12 (AC1009): view_center_point and view_height (as many other viewport attributes) are not usual
  295. # DXF attributes, they are stored as extended DXF tags.
  296. viewport = self.build_and_add_entity('VIEWPORT', attribs)
  297. viewport.dxf.id = viewport.get_next_viewport_id()
  298. with viewport.edit_data() as vp_data:
  299. vp_data.view_center_point = view_center_point
  300. vp_data.view_height = view_height
  301. return viewport
  302. class DXF12BlockLayout(BaseLayout):
  303. """
  304. BlockLayout has the same factory-function as Layout, but is managed
  305. in the BlocksSection() class. It represents a DXF Block definition.
  306. Attributes:
  307. _block_handle: db handle to BLOCK entity
  308. _endblk_handle: db handle to ENDBLK entity
  309. _entityspace: is the block content
  310. """
  311. def __init__(self, entitydb: 'EntityDB', dxffactory: 'DXFFactoryType', block_handle: str, endblk_handle: str):
  312. super(DXF12BlockLayout, self).__init__(dxffactory, EntitySpace(entitydb))
  313. self._block_handle = block_handle
  314. self._endblk_handle = endblk_handle
  315. # start of public interface
  316. def __contains__(self, entity: 'DXFEntity') -> bool:
  317. """
  318. Returns True if block contains entity else False. *entity* can be a handle-string, Tags(),
  319. ExtendedTags() or a wrapped entity.
  320. """
  321. if hasattr(entity, 'get_handle'):
  322. handle = entity.get_handle()
  323. elif hasattr(entity, 'dxf'): # it's a wrapped entity
  324. handle = entity.dxf.handle
  325. else:
  326. handle = entity
  327. return handle in self._entity_space
  328. @property
  329. def block(self) -> 'DXFEntity':
  330. """ Get associated BLOCK entity. """
  331. return self.get_entity_by_handle(self._block_handle)
  332. @property
  333. def endblk(self) -> 'DXFEntity':
  334. """ Get associated ENDBLK entity. """
  335. return self.get_entity_by_handle(self._endblk_handle)
  336. @property
  337. def name(self) -> str:
  338. """ Get block name """
  339. return self.block.dxf.name
  340. @name.setter
  341. def name(self, new_name) -> None:
  342. """ Set block name """
  343. block = self.block
  344. block.dxf.name = new_name
  345. block.dxf.name2 = new_name
  346. @property
  347. def is_layout_block(self) -> bool:
  348. """
  349. True if block is a model space or paper space block definition.
  350. """
  351. return self.block.is_layout_block
  352. def add_attdef(self, tag: str, insert: Sequence[float] = (0, 0), text: str = '',
  353. dxfattribs: dict = None) -> 'DXFEntity':
  354. """
  355. Add an :class:`Attdef` entity.
  356. Set position and alignment by the idiom::
  357. myblock.add_attdef('NAME').set_pos((2, 3), align='MIDDLE_CENTER')
  358. Args:
  359. tag: attribute name (tag) as string without spaces
  360. insert: attribute insert point relative to block origin (0, 0, 0)
  361. text: preset text for attribute
  362. """
  363. if dxfattribs is None:
  364. dxfattribs = {}
  365. dxfattribs['tag'] = tag
  366. dxfattribs['insert'] = insert
  367. dxfattribs['text'] = text
  368. return self.build_and_add_entity('ATTDEF', dxfattribs)
  369. def attdefs(self) -> Iterable['DXFEntity']:
  370. """
  371. Iterate for all :class:`Attdef` entities.
  372. """
  373. return (entity for entity in self if entity.dxftype() == 'ATTDEF')
  374. def has_attdef(self, tag: str) -> bool:
  375. """
  376. Returns `True` if an :class:`Attdef` for `tag` exists else `False`.
  377. Args:
  378. tag: tag name
  379. """
  380. return self.get_attdef(tag) is not None
  381. def get_attdef(self, tag: str) -> Optional['DXFEntity']:
  382. """
  383. Get attached :class:`Attdef` entity by `tag`.
  384. Args:
  385. tag: tag name
  386. Returns: :class:`Attdef`
  387. """
  388. for attdef in self.attdefs():
  389. if tag == attdef.dxf.tag:
  390. return attdef
  391. def get_attdef_text(self, tag: str, default: str = None) -> str:
  392. """
  393. Get content text for :class:`Attdef` `tag` as string or returns `default` if no :class:`Attdef` for `tag` exists.
  394. Args:
  395. tag: tag name
  396. default: default value if tag is absent
  397. """
  398. attdef = self.get_attdef(tag)
  399. if attdef is None:
  400. return default
  401. return attdef.dxf.text
  402. # end of public interface
  403. def add_entity(self, entity: 'DXFEntity') -> None:
  404. """
  405. Add an existing DXF entity to a layout, but be sure to unlink (:meth:`~Layout.unlink_entity()`) first the entity
  406. from the previous owner layout.
  407. Args:
  408. entity: :class:`DXFEntity`
  409. """
  410. self.add_handle(entity.dxf.handle)
  411. entity.dxf.paperspace = 0 # set a model space, because paper space layout is a different class
  412. for linked_entity in entity.linked_entities():
  413. linked_entity.dxf.paperspace = 0
  414. def add_handle(self, handle: str) -> None:
  415. """
  416. Add entity by handle to the block entity space.
  417. """
  418. self._entity_space.append(handle)
  419. def write(self, tagwriter: 'TagWriter') -> None:
  420. def write_tags(handle):
  421. tags = self._entity_space.get_tags_by_handle(handle)
  422. tagwriter.write_tags(tags)
  423. write_tags(self._block_handle)
  424. self._entity_space.write(tagwriter)
  425. write_tags(self._endblk_handle)
  426. def delete_all_entities(self) -> None:
  427. # 1. delete from database
  428. for handle in self._entity_space:
  429. del self.entitydb[handle]
  430. # 2. delete from entity space
  431. self._entity_space.delete_all_entities()
  432. def destroy(self) -> None:
  433. self.delete_all_entities()
  434. del self.entitydb[self._block_handle]
  435. del self.entitydb[self._endblk_handle]
  436. def get_const_attdefs(self) -> Iterable['DXFEntity']:
  437. """
  438. Returns a generator for constant ATTDEF entities.
  439. """
  440. return (attdef for attdef in self.attdefs() if attdef.is_const)