layouts.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  1. # Created: 21.03.2011
  2. # Copyright (c) 2011-2018, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, cast, Dict, Iterable, List, Union, Tuple, Any, Optional
  5. from ezdxf.entityspace import EntitySpace
  6. from ezdxf.legacy.layouts import DXF12Layout, DXF12BlockLayout
  7. from ezdxf.lldxf.extendedtags import ExtendedTags
  8. from ezdxf.lldxf.const import DXFKeyError, DXFValueError, DXFTypeError, STD_SCALES, DXFInternalEzdxfError
  9. from ezdxf.lldxf.validator import is_valid_name
  10. if TYPE_CHECKING:
  11. from ezdxf.eztypes import Drawing, TagWriter, DXFFactoryType, DXFDictionary, BlockRecord
  12. from ezdxf.eztypes import DXFEntity, Vertex, Viewport, GeoData, SortEntitiesTable
  13. PAPER_SPACE = '*Paper_Space'
  14. TMP_PAPER_SPACE_NAME = '*Paper_Space999999'
  15. class Layouts:
  16. def __init__(self, drawing: 'Drawing'):
  17. self.drawing = drawing
  18. self._layouts = {} # type: Dict[str, Layout]
  19. self._dxf_layout_management_table = None # type: DXFDictionary # key: layout name; value: layout_handle
  20. self._link_entities_section_into_blocks(drawing)
  21. self._setup()
  22. @staticmethod
  23. def _link_entities_section_into_blocks(drawing: 'Drawing') -> None:
  24. """
  25. Link entity spaces from entities section into associated block layouts.
  26. """
  27. blocks = drawing.blocks
  28. model_space_block = blocks.get('*MODEL_SPACE')
  29. model_space_block.set_entity_space(drawing.entities.model_space_entities())
  30. active_layout_block = blocks.get('*PAPER_SPACE')
  31. active_layout_block.set_entity_space(drawing.entities.active_layout_entities())
  32. drawing.entities.clear() # remove entities for entities section -> stored in blocks
  33. @property
  34. def dxffactory(self) -> 'DXFFactoryType':
  35. return self.drawing.dxffactory
  36. def _setup(self) -> None:
  37. """
  38. Setup layout management table.
  39. """
  40. layout_table_handle = self.drawing.rootdict['ACAD_LAYOUT']
  41. self._dxf_layout_management_table = cast('DXFDictionary', self.dxffactory.wrap_handle(layout_table_handle))
  42. # name ... layout name
  43. # handle ... handle to DXF object Layout
  44. for name, handle in self._dxf_layout_management_table.items():
  45. layout = Layout(self.drawing, handle)
  46. self._layouts[name] = layout
  47. def __len__(self) -> int:
  48. """
  49. Returns layout count.
  50. """
  51. return len(self._layouts)
  52. def __contains__(self, name: str) -> bool:
  53. """
  54. Returns if layout `name` exists.
  55. Args:
  56. name str: layout name
  57. """
  58. return name in self._layouts
  59. def __iter__(self) -> Iterable['Layout']:
  60. return iter(self._layouts.values())
  61. def modelspace(self) -> 'Layout':
  62. """
  63. Get model space layout.
  64. Returns:
  65. Layout: model space layout
  66. """
  67. return self.get('Model')
  68. def names(self) -> Iterable[str]:
  69. """
  70. Returns all layout names.
  71. Returns:
  72. Iterable[str]: layout names
  73. """
  74. return self._layouts.keys()
  75. def get(self, name: str) -> 'Layout':
  76. """
  77. Get layout by name.
  78. Args:
  79. name (str): layout name as shown in tab, e.g. ``Model`` for model space
  80. Returns:
  81. Layout: layout
  82. """
  83. if name is None:
  84. first_layout_name = self.names_in_taborder()[1]
  85. return self._layouts[first_layout_name]
  86. else:
  87. return self._layouts[name]
  88. def rename(self, old_name: str, new_name: str) -> None:
  89. """
  90. Rename a layout. Layout ``Model`` can not renamed and the new name of a layout must not exist.
  91. Args:
  92. old_name (str): actual layout name
  93. new_name (str): new layout name
  94. """
  95. if old_name == 'Model':
  96. raise ValueError('Can not rename model space.')
  97. if new_name in self._layouts:
  98. raise ValueError('Layout "{}" already exists.'.format(new_name))
  99. layout = self._layouts[old_name]
  100. del self._layouts[old_name]
  101. layout.dxf_layout.dxf.name = new_name
  102. self._layouts[new_name] = layout
  103. def names_in_taborder(self) -> List[str]:
  104. """
  105. Returns all layout names in tab order as a list of strings.
  106. """
  107. names = [(layout.dxf.taborder, name) for name, layout in self._layouts.items()]
  108. return [name for order, name in sorted(names)]
  109. def get_layout_for_entity(self, entity: 'DXFEntity') -> 'Layout':
  110. """
  111. Returns layout the `entity` resides in.
  112. Args:
  113. entity (DXFEntity): generic DXF entity
  114. """
  115. return self.get_layout_by_key(entity.dxf.owner)
  116. def get_layout_by_key(self, layout_key: str) -> 'Layout':
  117. """
  118. Returns a layout by its layout key.
  119. Args:
  120. layout_key (str): layout key
  121. """
  122. for layout in self._layouts.values():
  123. if layout_key == layout.layout_key:
  124. return layout
  125. raise DXFKeyError('Layout with key "{}" does not exist.'.format(layout_key))
  126. def new(self, name: str, dxfattribs: dict = None) -> 'Layout':
  127. """
  128. Create a new Layout.
  129. Args:
  130. name (str): layout name as shown in tab
  131. dxfattribs (dict): DXF attributes for the ``LAYOUT`` entity
  132. """
  133. if not is_valid_name(name):
  134. raise DXFValueError('name contains invalid characters')
  135. if dxfattribs is None:
  136. dxfattribs = {}
  137. if name in self._layouts:
  138. raise DXFValueError('Layout "{}" already exists'.format(name))
  139. def create_dxf_layout_entity() -> str:
  140. dxfattribs['name'] = name
  141. dxfattribs['owner'] = self._dxf_layout_management_table.dxf.handle
  142. dxfattribs.setdefault('taborder', len(self._layouts) + 1)
  143. dxfattribs['block_record'] = block_record_handle
  144. entity = self.drawing.objects.create_new_dxf_entity('LAYOUT', dxfattribs)
  145. return entity.dxf.handle
  146. block_layout = self.drawing.blocks.new_layout_block()
  147. block_record_handle = block_layout.block_record_handle
  148. block_record = block_layout.block_record
  149. layout_handle = create_dxf_layout_entity()
  150. block_record.dxf.layout = layout_handle
  151. # create valid layout entity
  152. layout = Layout(self.drawing, layout_handle)
  153. # add layout to management tables
  154. self._dxf_layout_management_table[name] = layout_handle
  155. self._layouts[name] = layout
  156. return layout
  157. def set_active_layout(self, name: str) -> None:
  158. """
  159. Set active paper space layout.
  160. Args:
  161. name (str): layout name as shown in tab
  162. """
  163. if name == 'Model': # reserved layout name
  164. raise DXFValueError('Can not set model space as active layout')
  165. new_active_layout = self.get(name) # raises KeyError if no layout 'name' exists
  166. old_active_layout_key = self.drawing.get_active_layout_key()
  167. if old_active_layout_key == new_active_layout.layout_key:
  168. return # layout 'name' is already the active layout
  169. blocks = self.drawing.blocks
  170. new_active_paper_space_name = new_active_layout.block_record_name
  171. blocks.rename_block(PAPER_SPACE, TMP_PAPER_SPACE_NAME)
  172. blocks.rename_block(new_active_paper_space_name, PAPER_SPACE)
  173. blocks.rename_block(TMP_PAPER_SPACE_NAME, new_active_paper_space_name)
  174. def delete(self, name: str) -> None:
  175. """
  176. Delete layout `name` and all entities in it.
  177. Args:
  178. name (str): layout name as shown in tabs
  179. Raises:
  180. KeyError: if layout `name` do not exists
  181. ValueError: if `name` is ``Model`` (deleting model space)
  182. """
  183. if name == 'Model':
  184. raise DXFValueError("Can not delete model space layout.")
  185. layout = self._layouts[name]
  186. if layout.layout_key == self.drawing.get_active_layout_key(): # name is the active layout
  187. for layout_name in self.names():
  188. if layout_name not in (name, 'Model'): # set any other layout as active layout
  189. self.set_active_layout(layout_name)
  190. break
  191. self._dxf_layout_management_table.remove(layout.name)
  192. del self._layouts[layout.name]
  193. layout.destroy()
  194. def active_layout(self) -> 'Layout':
  195. """
  196. Returns active paper space layout.
  197. """
  198. for layout in self:
  199. if layout.block_record_name.upper() == '*PAPER_SPACE':
  200. return layout
  201. raise DXFInternalEzdxfError('No active paper space found.')
  202. def write_entities_section(self, tagwriter: 'TagWriter') -> None:
  203. """
  204. Write ``ENTITIES`` section to DXF file, the ``ENTITIES`` section consist of all entities in model space and
  205. active paper space layout.
  206. All DXF entities of the remaining paper space layouts are stored in their associated ``BLOCK`` entity in the
  207. ``BLOCKS`` section.
  208. Args:
  209. tagwriter (TagWriter): tag writer object
  210. """
  211. self.modelspace().write(tagwriter)
  212. self.active_layout().write(tagwriter)
  213. class Layout(DXF12Layout):
  214. """
  215. Layout representation
  216. Every layout consist of a LAYOUT entity in the OBJECTS section, an associated BLOCK in the BLOCKS section and a
  217. BLOCK_RECORD_TABLE entry.
  218. layout_key: handle of the BLOCK_RECORD, every layout entity has this handle as owner attribute (entity.dxf.owner)
  219. There are 3 different layout types:
  220. 1. Model Space - not deletable, all entities of this layout are stored in the DXF file in the ENTITIES section, the
  221. associated ``*Model_Space`` block is empty, block name ``*Model_Space`` is mandatory, the layout name is
  222. ``Model`` and it is mandatory.
  223. 2. Active Layout - all entities of this layout are stored in the ENTITIES section, the associated ``*Paper_Space``
  224. block is empty, block name ``*Paper_Space`` is mandatory and also marks the active layout, the layout name can
  225. be an arbitrary string.
  226. 3. Inactive Layout - all entities of this layouts are stored in the associated BLOCK called ``*Paper_SpaceN``, where
  227. ``N`` is an arbitrary number, I don't know if the block name schema '*Paper_SpaceN' is mandatory, the layout
  228. name can be an arbitrary string.
  229. There is no different handling for active layouts and inactive layouts in ezdxf, this differentiation is just
  230. for AutoCAD important and it is not documented in the DXF reference.
  231. Internal Structure
  232. For **every** layout exists a BlockLayout() object in the BLOCKS section and a Layout() object in Layouts().
  233. The entity space of the BlockLayout() object and the entity space of the Layout() object are the **same** object.
  234. """
  235. # plot_layout_flags of LAYOUT entity
  236. PLOT_VIEWPORT_BORDERS = 1
  237. SHOW_PLOT_STYLES = 2
  238. PLOT_CENTERED = 4
  239. PLOT_HIDDEN = 8
  240. USE_STANDARD_SCALE = 16
  241. PLOT_PLOTSTYLES = 32
  242. SCALE_LINEWEIGHTS = 64
  243. PRINT_LINEWEIGHTS = 128
  244. DRAW_VIEWPORTS_FIRST = 512
  245. MODEL_TYPE = 1024
  246. UPDATE_PAPER = 2048
  247. ZOOM_TO_PAPER_ON_UPDATE = 4096
  248. INITIALIZING = 8192
  249. PREV_PLOT_INIT = 16384
  250. def __init__(self, drawing, layout_handle):
  251. dxffactory = drawing.dxffactory
  252. self.dxf_layout = dxffactory.wrap_handle(layout_handle)
  253. self._block_record_handle = self.dxf_layout.dxf.block_record
  254. entity_space = self._get_layout_entity_space(drawing, self.dxf_layout)
  255. super(Layout, self).__init__(entity_space, dxffactory, 0)
  256. self._layout_handle = layout_handle
  257. self._paperspace = 0 if self.name == 'Model' else 1
  258. self._repair_owner_tags()
  259. @staticmethod
  260. def _get_layout_entity_space(drawing: 'Drawing', layout: 'Layout') -> 'EntitySpace':
  261. block_record = drawing.dxffactory.wrap_handle(layout.dxf.block_record)
  262. block = drawing.blocks.get(block_record.dxf.name)
  263. return block.get_entity_space()
  264. def _repair_owner_tags(self) -> None:
  265. """
  266. Set `owner` and `paperspace` attributes of entities hosted by this layout to correct values.
  267. """
  268. layout_key = self.layout_key
  269. paper_space = self._paperspace
  270. for entity in self:
  271. if entity.get_dxf_attrib('owner', default=None) != layout_key:
  272. entity.set_dxf_attrib('owner', layout_key)
  273. if entity.get_dxf_attrib('paperspace', default=0) != paper_space:
  274. entity.set_dxf_attrib('paperspace', paper_space)
  275. # start of public interface
  276. def __contains__(self, entity: Union['DXFEntity', str]) -> bool:
  277. if isinstance(entity, str): # entity is a handle string
  278. entity = self.get_entity_by_handle(entity)
  279. return entity.dxf.owner == self.layout_key
  280. @property
  281. def name(self) -> str:
  282. """
  283. Returns layout name (as shown in tabs).
  284. """
  285. return self.dxf_layout.dxf.name
  286. @property
  287. def dxf(self) -> Any: # dynamic DXF attribute dispatching, e.g. DXFLayout.dxf.layout_flags
  288. """
  289. Returns the DXF name space attribute of the associated DXF ``LAYOUT`` entity.
  290. This enables direct access to the ``LAYOUT`` entity, e.g. Layout.dxf.layout_flags
  291. """
  292. return self.dxf_layout.dxf
  293. def page_setup(self, size: Tuple[int, int] = (297, 210),
  294. margins: Tuple[float, float, float, float] = (10, 15, 10, 15),
  295. units: str = 'mm',
  296. offset: Tuple[float, float] = (0, 0),
  297. rotation: int = 0,
  298. scale: int = 16,
  299. name: str = 'ezdxf',
  300. device: str = 'DWG to PDF.pc3') -> None:
  301. """
  302. Setup plot settings and paper size and reset viewports. All parameters in given `units` (mm or inch).
  303. Args:
  304. size: paper size as (width, height) tuple
  305. margins: (top, right, bottom, left) hint: clockwise
  306. units: "mm" or "inch"
  307. offset: plot origin offset is 2D point
  308. rotation: see table `Rotation`
  309. scale: int 0-32 = standard scale type or tuple(numerator, denominator) e.g. (1, 50) for 1:50
  310. name: paper name prefix '{name}_({width}_x_{height}_{unit})'
  311. device: device .pc3 configuration file or system printer name
  312. === ============
  313. int Rotation
  314. === ============
  315. 0 no rotation
  316. 1 90 degrees counter-clockwise
  317. 2 upside-down
  318. 3 90 degrees clockwise
  319. === ============
  320. """
  321. if self.name == 'Model':
  322. raise DXFTypeError("No paper setup for model space.")
  323. if int(rotation) not in (0, 1, 2, 3):
  324. raise DXFValueError("valid rotation values: 0-3")
  325. if isinstance(scale, tuple):
  326. standard_scale = 16
  327. elif isinstance(scale, int):
  328. standard_scale = scale
  329. scale = STD_SCALES.get(standard_scale, (1, 1))
  330. else:
  331. raise DXFTypeError("scale has to be an int or a tuple(numerator, denominator)")
  332. if scale[0] == 0:
  333. raise DXFValueError("scale numerator can't be 0.")
  334. if scale[1] == 0:
  335. raise DXFValueError("scale denominator can't be 0.")
  336. self.use_standard_scale(False) # works best, don't know why
  337. paper_width, paper_height = size
  338. margin_top, margin_right, margin_bottom, margin_left = margins
  339. units = units.lower()
  340. if units.startswith('inch'):
  341. units = 'Inches'
  342. plot_paper_units = 0
  343. unit_factor = 25.4 # inch to mm
  344. elif units == 'mm':
  345. units = 'MM'
  346. plot_paper_units = 1
  347. unit_factor = 1.0
  348. else:
  349. raise DXFValueError('Supported units: "mm" and "inch"')
  350. # Setup PLOTSETTINGS
  351. # all paper sizes in mm
  352. dxf = self.dxf_layout.dxf
  353. dxf.page_setup_name = ''
  354. dxf.plot_configuration_file = device
  355. dxf.paper_size = '{0}_({1:.2f}_x_{2:.2f}_{3})'.format(name, paper_width, paper_height, units)
  356. dxf.left_margin = margin_left * unit_factor
  357. dxf.bottom_margin = margin_bottom * unit_factor
  358. dxf.right_margin = margin_right * unit_factor
  359. dxf.top_margin = margin_top * unit_factor
  360. dxf.paper_width = paper_width * unit_factor
  361. dxf.paper_height = paper_height * unit_factor
  362. dxf.scale_numerator = scale[0]
  363. dxf.scale_denominator = scale[1]
  364. dxf.plot_paper_units = plot_paper_units
  365. dxf.plot_rotation = rotation
  366. x_offset, y_offset = offset
  367. dxf.plot_origin_x_offset = x_offset * unit_factor # conversion to mm
  368. dxf.plot_origin_y_offset = y_offset * unit_factor # conversion to mm
  369. dxf.standard_scale_type = standard_scale
  370. dxf.unit_factor = 1. / unit_factor # 1/1 for mm; 1/25.4 ... for inch
  371. # Setup Layout
  372. self.reset_paper_limits()
  373. self.reset_extends()
  374. self.reset_viewports()
  375. def reset_extends(self) -> None:
  376. """
  377. Reset paper space extends. (in :meth:`~Layout.page_setup` included)
  378. """
  379. dxf = self.dxf_layout.dxf
  380. dxf.extmin = (+1e20, +1e20, +1e20) # AutoCAD default
  381. dxf.extmax = (-1e20, -1e20, -1e20) # AutoCAD default
  382. def reset_paper_limits(self) -> None:
  383. """
  384. Set paper limits to default values, all values in paper space units but without plot scale (?).
  385. (in :meth:`~Layout.page_setup` included)
  386. """
  387. dxf = self.dxf_layout.dxf
  388. if dxf.plot_paper_units == 0: # inch
  389. unit_factor = 25.4
  390. else: # mm
  391. unit_factor = 1.0
  392. # all paper sizes are stored in mm
  393. paper_width = dxf.paper_width / unit_factor # in plot paper units
  394. paper_height = dxf.paper_height / unit_factor # in plot paper units
  395. left_margin = dxf.left_margin / unit_factor
  396. bottom_margin = dxf.bottom_margin / unit_factor
  397. x_offset = dxf.plot_origin_x_offset / unit_factor
  398. y_offset = dxf.plot_origin_y_offset / unit_factor
  399. # plot origin is the lower left corner of the printable paper area
  400. # limits are the paper borders relative to the plot origin
  401. shift_x = left_margin + x_offset
  402. shift_y = bottom_margin + y_offset
  403. dxf.limmin = (-shift_x, -shift_y) # paper space units
  404. dxf.limmax = (paper_width - shift_x, paper_height - shift_y)
  405. def get_paper_limits(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
  406. """
  407. Returns paper limits in plot paper units, relative to the plot origin, as tuple ((x1, y1), (x2, y2)).
  408. Lower left corner is (x1, y1), upper right corner is (x2, y2).
  409. plot origin = lower left corner of printable area + plot origin offset
  410. """
  411. return self.dxf.limmin, self.dxf.limmax
  412. def reset_viewports(self) -> None:
  413. """
  414. Delete all existing viewports, and add a new main viewport. (in :meth:`~Layout.page_setup` included)
  415. """
  416. # remove existing viewports
  417. for viewport in self.viewports():
  418. self.delete_entity(viewport)
  419. self.add_new_main_viewport()
  420. def add_new_main_viewport(self) -> None:
  421. """
  422. Add a new main viewport.
  423. """
  424. dxf = self.dxf_layout.dxf
  425. if dxf.plot_paper_units == 0: # inches
  426. unit_factor = 25.4
  427. else: # mm
  428. unit_factor = 1.0
  429. # all paper parameters in mm!
  430. # all viewport parameters in paper space units inch/mm + scale factor!
  431. scale_factor = dxf.scale_denominator / dxf.scale_numerator
  432. def paper_units(value):
  433. return value / unit_factor * scale_factor
  434. paper_width = paper_units(dxf.paper_width)
  435. paper_height = paper_units(dxf.paper_height)
  436. # plot origin offset
  437. x_offset = paper_units(dxf.plot_origin_x_offset)
  438. y_offset = paper_units(dxf.plot_origin_y_offset)
  439. # printing area
  440. printable_width = paper_width - paper_units(dxf.left_margin) - paper_units(dxf.right_margin)
  441. printable_height = paper_height - paper_units(dxf.bottom_margin) - paper_units(dxf.top_margin)
  442. # AutoCAD viewport (window) size
  443. vp_width = paper_width * 1.1
  444. vp_height = paper_height * 1.1
  445. # center of printing area
  446. center = (printable_width / 2 - x_offset, printable_height / 2 - y_offset)
  447. # create 'main' viewport
  448. main_viewport = self.add_viewport(
  449. center=center, # no influence to 'main' viewport?
  450. size=(vp_width, vp_height), # I don't get it, just use paper size!
  451. view_center_point=center, # same as center
  452. view_height=vp_height, # view height in paper space units
  453. )
  454. main_viewport.dxf.id = 1 # set as main viewport
  455. main_viewport.dxf.flags = 557088 # AutoCAD default value
  456. dxf.viewport = main_viewport.dxf.handle
  457. def set_plot_type(self, value: int = 5) -> None:
  458. """
  459. === ============================================================
  460. 0 last screen display
  461. 1 drawing extents
  462. 2 drawing limits
  463. 3 view specific (defined by Layout.dxf.plot_view_name)
  464. 4 window specific (defined by Layout.set_plot_window_limits())
  465. 5 layout information (default)
  466. === ============================================================
  467. Args:
  468. value: plot type
  469. Raises:
  470. DXFValueError: for `value` out of range
  471. """
  472. if 0 <= int(value) <= 5:
  473. self.dxf.plot_type = value # type: ignore
  474. else:
  475. raise DXFValueError('Plot type value out of range (0-5).')
  476. def set_plot_style(self, name: str = 'ezdxf.ctb', show: bool = False) -> None:
  477. """
  478. Set plot style file of type `ctb`.
  479. Args:
  480. name: plot style filename
  481. show: show plot style effect in preview? (AutoCAD specific attribute)
  482. """
  483. self.dxf_layout.dxf.current_style_sheet = name
  484. self.use_plot_styles(True)
  485. self.show_plot_styles(show)
  486. def set_plot_window(self,
  487. lower_left: Tuple[float, float] = (0, 0),
  488. upper_right: Tuple[float, float] = (0, 0)) -> None:
  489. """
  490. Set plot window size in (scaled) paper space units.
  491. Args:
  492. lower_left: lower left corner as 2D point
  493. upper_right: upper right corner as 2D point
  494. """
  495. x1, y1 = lower_left
  496. x2, y2 = upper_right
  497. dxf = self.dxf_layout.dxf
  498. dxf.plot_window_x1 = x1
  499. dxf.plot_window_y1 = y1
  500. dxf.plot_window_x2 = x2
  501. dxf.plot_window_y2 = y2
  502. self.set_plot_type(4)
  503. # plot layout flags setter
  504. def plot_viewport_borders(self, state: bool = True) -> None:
  505. self.set_plot_flags(self.PLOT_VIEWPORT_BORDERS, state)
  506. def show_plot_styles(self, state: bool = True) -> None:
  507. self.set_plot_flags(self.SHOW_PLOT_STYLES, state)
  508. def plot_centered(self, state: bool = True) -> None:
  509. self.set_plot_flags(self.PLOT_CENTERED, state)
  510. def plot_hidden(self, state: bool = True) -> None:
  511. self.set_plot_flags(self.PLOT_HIDDEN, state)
  512. def use_standard_scale(self, state: bool = True) -> None:
  513. self.set_plot_flags(self.USE_STANDARD_SCALE, state)
  514. def use_plot_styles(self, state: bool = True) -> None:
  515. self.set_plot_flags(self.PLOT_PLOTSTYLES, state)
  516. def scale_lineweights(self, state: bool = True) -> None:
  517. self.set_plot_flags(self.SCALE_LINEWEIGHTS, state)
  518. def print_lineweights(self, state: bool = True) -> None:
  519. self.set_plot_flags(self.PRINT_LINEWEIGHTS, state)
  520. def draw_viewports_first(self, state: bool = True) -> None:
  521. self.set_plot_flags(self.PRINT_LINEWEIGHTS, state)
  522. def model_type(self, state: bool = True) -> None:
  523. self.set_plot_flags(self.MODEL_TYPE, state)
  524. def update_paper(self, state: bool = True) -> None:
  525. self.set_plot_flags(self.UPDATE_PAPER, state)
  526. def zoom_to_paper_on_update(self, state: bool = True) -> None:
  527. self.set_plot_flags(self.ZOOM_TO_PAPER_ON_UPDATE, state)
  528. def plot_flags_initializing(self, state: bool = True) -> None:
  529. self.set_plot_flags(self.INITIALIZING, state)
  530. def prev_plot_init(self, state: bool = True) -> None:
  531. self.set_plot_flags(self.PREV_PLOT_INIT, state)
  532. def set_plot_flags(self, flag, state: bool = True) -> None:
  533. self.dxf_layout.set_flag_state(flag, state=state, name='plot_layout_flags')
  534. def add_viewport(self, center: 'Vertex',
  535. size: Tuple[float, float],
  536. view_center_point: 'Vertex',
  537. view_height: float,
  538. dxfattribs: dict = None) -> 'Viewport':
  539. dxfattribs = dxfattribs or {}
  540. width, height = size
  541. attribs = {
  542. 'center': center,
  543. 'width': width,
  544. 'height': height,
  545. 'status': 1, # by default highest priority (stack order)
  546. 'layer': 'VIEWPORTS', # use separated layer to turn off for plotting
  547. 'view_center_point': view_center_point,
  548. 'view_height': view_height,
  549. }
  550. attribs.update(dxfattribs)
  551. viewport = cast('Viewport', self.build_and_add_entity('VIEWPORT', attribs))
  552. viewport.dxf.id = viewport.get_next_viewport_id()
  553. return viewport
  554. # end of public interface
  555. @property
  556. def layout_key(self) -> str:
  557. """
  558. Returns the layout key as string.
  559. The layout key is the handle of the associated ``BLOCK_RECORD`` entry in the ``BLOCK_RECORDS`` table.
  560. """
  561. return self._block_record_handle
  562. @property
  563. def block_record(self) -> 'BlockRecord':
  564. """
  565. Returns the associated ``BLOCK_RECORD``.
  566. """
  567. return self.drawing.dxffactory.wrap_handle(self._block_record_handle)
  568. @property
  569. def block_record_name(self) -> str:
  570. """
  571. Returns the name of the associated ``BLOCK_RECORD`` as string.
  572. """
  573. return self.block_record.dxf.name
  574. @property
  575. def block(self) -> 'BlockLayout':
  576. """
  577. Returns the associated `BlockLayout` object.
  578. """
  579. return self.drawing.blocks.get(self.block_record_name)
  580. def _set_paperspace(self, entity: 'DXFEntity') -> None:
  581. """
  582. Set correct `owner` and `paperspace` attribute, to be a valid member of this layout.
  583. Args:
  584. entity (DXFEntiy): generic DXF entity
  585. """
  586. entity.dxf.paperspace = self._paperspace
  587. entity.dxf.owner = self.layout_key
  588. def destroy(self) -> None:
  589. """
  590. Delete all member entities and the layout itself from entity database and all other structures.
  591. """
  592. self.delete_all_entities()
  593. self.drawing.blocks.delete_block(self.block.name)
  594. self.drawing.objects.remove_handle(self._layout_handle)
  595. self.drawing.entitydb.delete_handle(self._layout_handle)
  596. def get_extension_dict(self, create: bool = True) -> 'DXFDictionary':
  597. """
  598. Returns the associated extension dictionary.
  599. Args:
  600. create (bool): create extension dictionary if not exists
  601. Raises:
  602. DXFValueError: if extension dictionary do not exists and `create` is False
  603. """
  604. block_record = self.block_record
  605. try:
  606. xdict = block_record.get_extension_dict()
  607. except (DXFValueError, DXFKeyError):
  608. # DXFValueError: block_record has no extension dict
  609. # DXFKeyError: block_record has an extension dict handle, but extension dict does not exist
  610. if create:
  611. xdict = block_record.new_extension_dict()
  612. else:
  613. raise DXFValueError('Extension dictionary do not exist.')
  614. return xdict
  615. def new_geodata(self, dxfattribs: dict = None) -> 'GeoData':
  616. """
  617. Creates a new :class:`GeoData` entity and replaces existing ones. The GEODATA entity resides in the OBJECTS section
  618. and NOT in the layout entity space and it is linked to the layout by an extension dictionary located in BLOCK_RECORD
  619. of the layout.
  620. The GEODATA entity requires DXF version R2010+. The DXF Reference does not document if other layouts than model
  621. space supports geo referencing, so getting/setting geo data may only make sense for the model space layout, but
  622. it is also available in paper space layouts.
  623. Args:
  624. dxfattribs (dict): DXF attributes for the :class:`GeoData` entity
  625. """
  626. if dxfattribs is None:
  627. dxfattribs = {}
  628. dwg = self.drawing
  629. if dwg.dxfversion < 'AC1024':
  630. raise DXFValueError('GEODATA entity requires DXF version R2010 (AC1024) or later.')
  631. xdict = self.get_extension_dict(create=True)
  632. geodata = dwg.objects.add_geodata(
  633. owner=xdict.dxf.handle,
  634. dxfattribs=dxfattribs,
  635. )
  636. xdict['ACAD_GEOGRAPHICDATA'] = geodata.dxf.handle
  637. return geodata
  638. def get_geodata(self) -> Optional['GeoData']:
  639. """
  640. Returns the :class:`GeoData` entity associated to this layout or None.
  641. """
  642. try:
  643. xdict = self.block_record.get_extension_dict()
  644. except DXFValueError:
  645. return None
  646. try:
  647. return xdict.get_entity('ACAD_GEOGRAPHICDATA')
  648. except DXFKeyError:
  649. return None
  650. def get_sortents_table(self, create: bool = True) -> 'SortEntitiesTable':
  651. """
  652. Get/Create to layout associated ``SORTENTSTABLE`` object.
  653. Args:
  654. create (bool): new table if table do not exists and create is True
  655. Raises:
  656. DXFValueError: if table not exists and `create` is False
  657. """
  658. xdict = self.get_extension_dict(create=True)
  659. try:
  660. sortents_table = xdict.get_entity('ACAD_SORTENTS')
  661. except DXFKeyError:
  662. if create:
  663. sortents_table = self.drawing.objects.create_new_dxf_entity(
  664. 'SORTENTSTABLE',
  665. dxfattribs={
  666. 'owner': xdict.dxf.handle,
  667. 'block_record': self.layout_key
  668. },
  669. )
  670. xdict['ACAD_SORTENTS'] = sortents_table.dxf.handle
  671. else:
  672. raise DXFValueError('Extension dictionary entry ACAD_SORTENTS do not exist.')
  673. return sortents_table
  674. def set_redraw_order(self, handles: Union[Dict, Iterable[Tuple[str, str]]]) -> None:
  675. """
  676. If the header variable $SORTENTS `Regen` flag (bit-code value 16) is set, AutoCAD regenerates entities in
  677. ascending handles order.
  678. To change redraw order associate a different sort handle to entities, this redefines the order in which the
  679. entities are regenerated. `handles` can be a dict of object_handle and sort_handle as (key, value) pairs, or an
  680. iterable of (object_handle, sort_handle) tuples.
  681. The sort_handle doesn't have to be unique, same or all handles can share the same sort_handle and sort_handles
  682. can use existing handles too.
  683. The '0' handle can be used, but this sort_handle will be drawn as latest (on top of all other entities) and not
  684. as first as expected.
  685. Args:
  686. handles: iterable or dict of handle associations; for iterable an association
  687. is a tuple (object_handle, sort_handle); for dict the association is
  688. key: object_handle, value: sort_handle
  689. """
  690. sortents = self.get_sortents_table()
  691. if isinstance(handles, dict):
  692. handles = handles.items()
  693. sortents.set_handles(handles)
  694. def get_redraw_order(self) -> Iterable[Tuple[str, str]]:
  695. """
  696. Returns iterable for all existing table entries as (object_handle, sort_handle) pairs.
  697. (see also :meth:`~Layout.set_redraw_order`)
  698. """
  699. empty = []
  700. try:
  701. xdict = self.get_extension_dict(create=False)
  702. except DXFValueError:
  703. return empty
  704. try:
  705. sortents_table = xdict.get_entity('ACAD_SORTENTS')
  706. except DXFKeyError:
  707. return empty
  708. return iter(sortents_table)
  709. class BlockLayout(DXF12BlockLayout):
  710. def add_entity(self, entity: 'DXFEntity') -> None:
  711. """
  712. Add an existing DXF entity to a layout, but be sure to unlink (:meth:`~Layout.unlink_entity()`) first the entity
  713. from the previous owner layout.
  714. Args:
  715. entity: :class:`DXFEntity`
  716. """
  717. # entity can be ExtendedTags() or a GraphicEntity() or inherited wrapper class
  718. if isinstance(entity, ExtendedTags):
  719. entity = self._dxffactory.wrap_entity(entity)
  720. entity.dxf.owner = self.block_record_handle
  721. entity.dxf.paperspace = 0 # set a model space, because paper space layout is a different class
  722. for linked_entity in entity.linked_entities():
  723. linked_entity.dxf.owner = self.block_record_handle
  724. linked_entity.dxf.paperspace = 0
  725. self._entity_space.append(entity.dxf.handle)
  726. @property
  727. def block_record_handle(self) -> str:
  728. return self.block.dxf.owner
  729. def set_block_record_handle(self, block_record_handle: str) -> None:
  730. self.block.dxf.owner = block_record_handle
  731. self.endblk.dxf.owner = block_record_handle
  732. @property
  733. def block_record(self) -> 'BlockRecord':
  734. return self.drawing.dxffactory.wrap_handle(self.block_record_handle)
  735. def get_entity_space(self) -> EntitySpace:
  736. return self._entity_space
  737. def set_entity_space(self, entity_space: EntitySpace) -> None:
  738. self._entity_space = entity_space
  739. def destroy(self) -> None:
  740. self.drawing.sections.tables.block_records.remove_handle(self.block_record_handle)
  741. super(BlockLayout, self).destroy()
  742. def write(self, tagwriter: 'TagWriter'):
  743. # BLOCK section: do not write content of model space and active layout
  744. if self.name.upper() in ('*MODEL_SPACE', '*PAPER_SPACE'):
  745. save = self._entity_space
  746. self._entity_space = EntitySpace(self.entitydb)
  747. super(BlockLayout, self).write(tagwriter)
  748. self._entity_space = save
  749. else:
  750. super(BlockLayout, self).write(tagwriter)