This commit is contained in:
Christian Anetzberger
2026-01-22 20:23:51 +01:00
commit a197de9456
4327 changed files with 1235205 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from .base import BaseLayout, VirtualLayout
from .layout import Layout, Modelspace, Paperspace
from .blocklayout import BlockLayout
from .layouts import Layouts

View File

@@ -0,0 +1,468 @@
# Copyright (c) 2019-2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterator, Union, Iterable, Optional
from ezdxf.entities import factory, is_graphic_entity, SortEntsTable
from ezdxf.enums import InsertUnits
from ezdxf.lldxf.const import (
DXFKeyError,
DXFValueError,
DXFStructureError,
LATEST_DXF_VERSION,
DXFTypeError,
)
from ezdxf.query import EntityQuery
from ezdxf.groupby import groupby
from ezdxf.entitydb import EntityDB, EntitySpace
from ezdxf.graphicsfactory import CreatorInterface
if TYPE_CHECKING:
from ezdxf.entities import DXFGraphic, BlockRecord, ExtensionDict
from ezdxf.eztypes import KeyFunc
SUPPORTED_FOREIGN_ENTITY_TYPES = {
"ARC",
"LINE",
"CIRCLE",
"ELLIPSE",
"POINT",
"LWPOLYLINE",
"SPLINE",
"3DFACE",
"SOLID",
"TRACE",
"SHAPE",
"POLYLINE",
"MESH",
"TEXT",
"MTEXT",
"HATCH",
"ATTRIB",
"ATTDEF",
}
class _AbstractLayout(CreatorInterface):
entity_space = EntitySpace()
@property
def entitydb(self) -> EntityDB:
"""Returns drawing entity database. (internal API)"""
return self.doc.entitydb
def rename(self, name) -> None:
pass
def __len__(self) -> int:
"""Returns count of entities owned by the layout."""
return len(self.entity_space)
def __iter__(self) -> Iterator[DXFGraphic]:
"""Returns iterable of all drawing entities in this layout."""
return iter(self.entity_space) # type: ignore
def __getitem__(self, index):
"""Get entity at `index`.
The underlying data structure for storing entities is organized like a
standard Python list, therefore `index` can be any valid list indexing
or slicing term, like a single index ``layout[-1]`` to get the last
entity, or an index slice ``layout[:10]`` to get the first 10 or less
entities as ``list[DXFGraphic]``.
"""
return self.entity_space[index]
def query(self, query: str = "*") -> EntityQuery:
"""Get all DXF entities matching the :ref:`entity query string`."""
return EntityQuery(iter(self), query)
def groupby(self, dxfattrib: str = "", key: Optional[KeyFunc] = None) -> dict:
"""
Returns a ``dict`` of entity lists, where entities are grouped by a
`dxfattrib` or a `key` function.
Args:
dxfattrib: grouping by DXF attribute like ``'layer'``
key: key function, which accepts a :class:`DXFGraphic` entity as
argument and returns the grouping key of an entity or ``None``
to ignore the entity. Reason for ignoring: a queried DXF
attribute is not supported by entity.
"""
return groupby(iter(self), dxfattrib, key)
def destroy(self):
pass
def purge(self):
"""Remove all destroyed entities from the layout entity space."""
self.entity_space.purge()
class BaseLayout(_AbstractLayout):
def __init__(self, block_record: BlockRecord):
doc = block_record.doc
assert doc is not None
super().__init__(doc)
self.entity_space = block_record.entity_space
# This is the real central layout management structure:
self.block_record: BlockRecord = block_record
@property
def block_record_handle(self):
"""Returns block record handle. (internal API)"""
return self.block_record.dxf.handle
@property
def layout_key(self) -> str:
"""Returns the layout key as hex string.
The layout key is the handle of the associated BLOCK_RECORD entry in the
BLOCK_RECORDS table.
(internal API)
"""
return self.block_record.dxf.handle
@property
def is_alive(self):
"""``False`` if layout is deleted."""
return self.block_record.is_alive
@property
def is_active_paperspace(self) -> bool:
"""``True`` if is active layout."""
return self.block_record.is_active_paperspace
@property
def is_any_paperspace(self) -> bool:
"""``True`` if is any kind of paperspace layout."""
return self.block_record.is_any_paperspace
@property
def is_modelspace(self) -> bool:
"""``True`` if is modelspace layout."""
return self.block_record.is_modelspace
@property
def is_any_layout(self) -> bool:
"""``True`` if is any kind of modelspace or paperspace layout."""
return self.block_record.is_any_layout
@property
def is_block_layout(self) -> bool:
"""``True`` if not any kind of modelspace or paperspace layout, just a
regular block definition.
"""
return self.block_record.is_block_layout
@property
def units(self) -> InsertUnits:
"""Get/Set layout/block drawing units as enum, see also :ref:`set
drawing units`.
"""
# todo: doesn't care about units stored in XDATA, see ezdxf/units.py
# Don't know what this units are used for, but header var $INSUNITS
# are the real units of the model space.
return self.block_record.dxf.units
@units.setter
def units(self, value: InsertUnits) -> None:
"""Set layout/block drawing units as enum."""
self.block_record.dxf.units = value # has a DXF validator
def get_extension_dict(self) -> ExtensionDict:
"""Returns the associated extension dictionary, creates a new one if
necessary.
"""
block_record = self.block_record
if block_record.has_extension_dict:
return block_record.get_extension_dict()
else:
return block_record.new_extension_dict()
def add_entity(self, entity: DXFGraphic) -> None:
"""Add an existing :class:`DXFGraphic` entity to a layout, but be sure
to unlink (:meth:`~BaseLayout.unlink_entity`) `entity` from the previous
owner layout. Adding entities from a different DXF drawing is not
supported.
.. warning::
This is a low-level tool - use it with caution and make sure you understand
what you are doing! If used improperly, the DXF document may be damaged.
"""
# bind virtual entities to the DXF document:
doc = self.doc
if entity.dxf.handle is None and doc:
factory.bind(entity, doc)
handle = entity.dxf.handle
if handle is None or handle not in self.doc.entitydb:
raise DXFStructureError(
"Adding entities from a different DXF drawing is not supported."
)
if not is_graphic_entity(entity):
raise DXFTypeError(f"invalid entity {str(entity)}")
self.block_record.add_entity(entity)
def add_foreign_entity(self, entity: DXFGraphic, copy=True) -> None:
"""Add a foreign DXF entity to a layout, this foreign entity could be
from another DXF document or an entity without an assigned DXF document.
The intention of this method is to add **simple** entities from another
DXF document or from a DXF iterator, for more complex operations use the
:mod:`~ezdxf.addons.importer` add-on. Especially objects with BLOCK
section (INSERT, DIMENSION, MLEADER) or OBJECTS section dependencies
(IMAGE, UNDERLAY) can not be supported by this simple method.
Not all DXF types are supported and every dependency or resource
reference from another DXF document will be removed except attribute
layer will be preserved but only with default attributes like
color ``7`` and linetype ``CONTINUOUS`` because the layer attribute
doesn't need a layer table entry.
If the entity is part of another DXF document, it will be unlinked from
this document and its entity database if argument `copy` is ``False``,
else the entity will be copied. Unassigned entities like from DXF
iterators will just be added.
Supported DXF types:
- POINT
- LINE
- CIRCLE
- ARC
- ELLIPSE
- LWPOLYLINE
- SPLINE
- POLYLINE
- 3DFACE
- SOLID
- TRACE
- SHAPE
- MESH
- ATTRIB
- ATTDEF
- TEXT
- MTEXT
- HATCH
Args:
entity: DXF entity to copy or move
copy: if ``True`` copy entity from other document else unlink from
other document
Raises:
CopyNotSupported: copying of `entity` i not supported
"""
foreign_doc = entity.doc
dxftype = entity.dxftype()
if dxftype not in SUPPORTED_FOREIGN_ENTITY_TYPES:
raise DXFTypeError(f"unsupported DXF type: {dxftype}")
if foreign_doc is self.doc:
raise DXFValueError("entity from same DXF document")
if foreign_doc is not None:
if copy:
entity = entity.copy()
else:
# Unbind entity from other document without destruction.
factory.unbind(entity)
entity.remove_dependencies(self.doc)
# add to this layout & bind to document
self.add_entity(entity)
def unlink_entity(self, entity: DXFGraphic) -> None:
"""Unlink `entity` from layout but does not delete entity from the
entity database, this removes `entity` just from the layout entity space.
"""
self.block_record.unlink_entity(entity)
def delete_entity(self, entity: DXFGraphic) -> None:
"""Delete `entity` from layout entity space and the entity database,
this destroys the `entity`.
"""
self.block_record.delete_entity(entity)
def delete_all_entities(self) -> None:
"""Delete all entities from this layout and from entity database,
this destroys all entities in this layout.
"""
# Create list, because delete modifies the base data structure of
# the iterator:
for entity in list(self):
self.delete_entity(entity)
def move_to_layout(self, entity: DXFGraphic, layout: BaseLayout) -> None:
"""Move entity to another layout.
Args:
entity: DXF entity to move
layout: any layout (modelspace, paperspace, block) from
**same** drawing
"""
if entity.doc != layout.doc:
raise DXFStructureError(
"Moving between different DXF drawings is not supported."
)
try:
self.unlink_entity(entity)
except ValueError:
raise DXFValueError("Layout does not contain entity.")
else:
layout.add_entity(entity)
def destroy(self) -> None:
"""Delete all linked resources. (internal API)"""
# block_records table is owner of block_record has to delete it
# the block_record is the owner of the entities and deletes them all
self.doc.block_records.remove(self.block_record.dxf.name)
def get_sortents_table(self, create: bool = True) -> SortEntsTable:
"""Get/Create the SORTENTSTABLE object associated to the layout.
Args:
create: new table if table do not exist and `create` is ``True``
Raises:
DXFValueError: if table not exist and `create` is ``False``
(internal API)
"""
xdict = self.get_extension_dict()
try:
sortents_table = xdict["ACAD_SORTENTS"]
except DXFKeyError:
if create:
sortents_table = self.doc.objects.new_entity(
"SORTENTSTABLE",
dxfattribs={
"owner": xdict.handle,
"block_record_handle": self.layout_key,
},
)
xdict["ACAD_SORTENTS"] = sortents_table
else:
raise DXFValueError(
"Extension dictionary entry ACAD_SORTENTS does not exist."
)
return sortents_table
def set_redraw_order(self, handles: Union[dict, Iterable[tuple[str, str]]]) -> None:
"""If the header variable $SORTENTS `Regen` flag (bit-code value 16)
is set, AutoCAD regenerates entities in ascending handles order.
To change redraw order associate a different sort-handle to entities,
this redefines the order in which the entities are regenerated.
The `handles` argument can be a dict of entity_handle and sort_handle
as (k, v) pairs, or an iterable of (entity_handle, sort_handle) tuples.
The sort-handle doesn't have to be unique, some or all entities can
share the same sort-handle and a sort-handle can be an existing handle.
The "0" handle can be used, but this sort-handle will be drawn as
latest (on top of all other entities) and not as first as expected.
Args:
handles: iterable or dict of handle associations; an iterable
of 2-tuples (entity_handle, sort_handle) or a dict (k, v)
association as (entity_handle, sort_handle)
"""
sortents = self.get_sortents_table()
if isinstance(handles, dict):
handles = handles.items()
sortents.set_handles(handles)
def get_redraw_order(self) -> Iterable[tuple[str, str]]:
"""Returns iterable for all existing table entries as (entity_handle,
sort_handle) pairs, see also :meth:`~BaseLayout.set_redraw_order`.
"""
if self.block_record.has_extension_dict:
xdict = self.get_extension_dict()
else:
return tuple()
try:
sortents_table = xdict["ACAD_SORTENTS"]
except DXFKeyError:
return tuple()
return iter(sortents_table)
def entities_in_redraw_order(self, reverse=False) -> Iterable[DXFGraphic]:
"""Yields all entities from layout in ascending redraw order or
descending redraw order if `reverse` is ``True``.
"""
from ezdxf import reorder
redraw_order = self.get_redraw_order()
if reverse:
return reorder.descending(self.entity_space, redraw_order) # type: ignore
return reorder.ascending(self.entity_space, redraw_order) # type: ignore
class VirtualLayout(_AbstractLayout):
"""Helper class to disassemble complex entities into basic DXF
entities by rendering into a virtual layout.
All entities do not have an assigned DXF document and therefore
are not stored in any entity database and can not be added to another
layout by :meth:`add_entity`.
Deleting entities from this layout does not destroy the entity!
"""
def __init__(self):
super().__init__(None)
self.entity_space = EntitySpace()
@property
def dxfversion(self) -> str:
return LATEST_DXF_VERSION
def add_entity(self, entity: DXFGraphic) -> None:
self.entity_space.add(entity)
def new_entity(self, type_: str, dxfattribs: dict) -> DXFGraphic:
entity = factory.new(type_, dxfattribs=dxfattribs)
self.entity_space.add(entity)
return entity # type: ignore
def unlink_entity(self, entity: DXFGraphic) -> None:
self.entity_space.remove(entity)
def delete_entity(self, entity: DXFGraphic) -> None:
self.entity_space.remove(entity)
def delete_all_entities(self) -> None:
self.entity_space.clear()
def copy_all_to_layout(self, layout: BaseLayout) -> None:
"""Copy all entities to a real document layout."""
doc = layout.doc
entitydb = doc.entitydb
for entity in self.entity_space:
try:
clone = entity.copy()
except DXFTypeError:
continue
clone.doc = doc
entitydb.add(clone)
layout.add_entity(clone) # type: ignore
def move_all_to_layout(self, layout: BaseLayout) -> None:
"""Move all entities to a real document layout."""
doc = layout.doc
entitydb = doc.entitydb
for entity in self.entity_space:
entity.doc = doc
entitydb.add(entity)
layout.add_entity(entity) # type: ignore
self.delete_all_entities()

View File

@@ -0,0 +1,132 @@
# Copyright (c) 2019-2021, Manfred Moitzi
# License: MIT License
from typing import Iterable, Optional
from ezdxf.math import Vec3, UVec
from ezdxf.lldxf import const
from .base import BaseLayout
from ezdxf.entities import DXFGraphic, AttDef, Block, EndBlk
class BlockLayout(BaseLayout):
"""BlockLayout has the same factory-functions as Layout, but is managed
in the :class:`BlocksSection` class. It represents a DXF Block.
"""
def __contains__(self, entity) -> bool:
"""Returns ``True`` if block contains `entity`.
Args:
entity: :class:`DXFGraphic` object or handle as hex string
"""
if isinstance(entity, str):
entity = self.entitydb[entity]
return entity in self.entity_space
@property
def block(self) -> Optional[Block]:
"""the associated :class:`~ezdxf.entities.Block` entity."""
return self.block_record.block
@property
def endblk(self) -> Optional[EndBlk]:
"""the associated :class:`~ezdxf.entities.EndBlk` entity."""
return self.block_record.endblk
@property
def name(self) -> str:
"""Get/set the BLOCK name"""
return self.block_record.dxf.name
@name.setter
def name(self, new_name) -> None:
self.block_record.rename(new_name)
@property
def dxf(self):
"""DXF name space of associated :class:`~ezdxf.entities.BlockRecord`
table entry.
"""
return self.block_record.dxf
@property
def can_explode(self) -> bool:
"""Set property to ``True`` to allow exploding block references of
this block.
"""
return bool(self.block_record.dxf.explode)
@can_explode.setter
def can_explode(self, value: bool):
self.block_record.dxf.explode = int(value)
@property
def scale_uniformly(self) -> bool:
"""Set property to ``True`` to allow block references of this block
only scale uniformly.
"""
return bool(self.block_record.dxf.scale)
@scale_uniformly.setter
def scale_uniformly(self, value: bool):
self.block_record.dxf.scale = int(value)
@property
def base_point(self) -> Vec3:
"""Get/Set the base point of the block."""
return Vec3(self.block.dxf.base_point) # type: ignore
@base_point.setter
def base_point(self, value: UVec) -> None:
self.block.dxf.base_point = Vec3(value) # type: ignore
def attdefs(self) -> Iterable[AttDef]:
"""Returns iterable of all :class:`~ezdxf.entities.attrib.Attdef`
entities.
"""
return (e for e in self if isinstance(e, AttDef))
def has_attdef(self, tag: str) -> bool:
"""Returns ``True`` if an :class:`~ezdxf.entities.attrib.Attdef` for
`tag` exist.
"""
return self.get_attdef(tag) is not None
def get_attdef(self, tag: str) -> Optional[DXFGraphic]:
"""Returns attached :class:`~ezdxf.entities.attrib.Attdef` entity by
`tag` name.
"""
for attdef in self.attdefs():
if tag == attdef.dxf.tag:
return attdef
return None
def get_attdef_text(self, tag: str, default: str = "") -> str:
"""Returns text content for :class:`~ezdxf.entities.attrib.Attdef`
`tag` as string or returns `default` if no :class:`Attdef` for `tag`
exist.
Args:
tag: name of tag
default: default value if `tag` not exist
"""
attdef = self.get_attdef(tag)
if attdef is None:
return default
return attdef.dxf.text
def get_const_attdefs(self) -> Iterable[AttDef]:
"""Returns iterable for all constant ATTDEF entities. (internal API)"""
return (attdef for attdef in self.attdefs() if attdef.is_const)
def has_non_const_attdef(self) -> bool:
"""Returns ``True`` if the block has a non constant attribute
definition.
"""
return any(not attdef.is_const for attdef in self.attdefs())
def update_block_flags(self):
state = self.has_non_const_attdef()
self.block.set_flag_state(const.BLK_NON_CONSTANT_ATTRIBUTES, state)

View File

@@ -0,0 +1,862 @@
# Copyright (c) 2019-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Union,
cast,
Optional,
)
import logging
from ezdxf.math import Vec2, UVec
from ezdxf.entitydb import EntitySpace
from ezdxf.lldxf import const
from ezdxf.lldxf.validator import make_table_key
from .base import BaseLayout
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.layouts import BlockLayout
from ezdxf.entities import GeoData, Viewport, DXFLayout, DXFGraphic, BlockRecord
logger = logging.getLogger("ezdxf")
def get_block_entity_space(doc: Drawing, block_record_handle: str) -> EntitySpace:
block_record = doc.entitydb[block_record_handle]
return block_record.entity_space # type: ignore
class Layout(BaseLayout):
"""
Layout representation - base class for :class:`Modelspace` and
:class:`Paperspace`
Every layout consist of a LAYOUT entity in the OBJECTS section, an
associated BLOCK in the BLOCKS section and a BLOCK_RECORD_TABLE entry.
layout_key: handle of the BLOCK_RECORD, every layout entity has this
handle as owner attribute (entity.dxf.owner)
There are 3 different layout types:
1. :class:`Modelspace`
2. active :class:`Paperspace` layout
3. inactive :class:`Paperspace` layout
Internal Structure
For every layout exist a :class:`BlockLayout` object in
:class:`BlocksSection` and a :class:`Layout` object
(as :class:`Modelspace` or :class:`Paperspace`) in :class:`Layouts`.
The entity space of the :class:`BlockLayout` object and the entity space
of the :class:`Layout` object are the same object.
"""
# plot_layout_flags of LAYOUT entity
PLOT_VIEWPORT_BORDERS = 1
SHOW_PLOT_STYLES = 2
PLOT_CENTERED = 4
PLOT_HIDDEN = 8
USE_STANDARD_SCALE = 16
PLOT_PLOTSTYLES = 32
SCALE_LINEWEIGHTS = 64
PRINT_LINEWEIGHTS = 128
DRAW_VIEWPORTS_FIRST = 512
MODEL_TYPE = 1024
UPDATE_PAPER = 2048
ZOOM_TO_PAPER_ON_UPDATE = 4096
INITIALIZING = 8192
PREV_PLOT_INIT = 16384
def __init__(self, layout: DXFLayout, doc: Drawing):
self.dxf_layout = layout
handle = layout.dxf.get("block_record_handle", "0")
try:
block_record = doc.entitydb[handle]
except KeyError:
block_record = _find_layout_block_record(layout) # type: ignore
if block_record is None:
raise const.DXFStructureError(
f"required BLOCK_RECORD #{handle} for layout '{layout.dxf.name}' "
f"does not exist"
)
if block_record.dxftype() != "BLOCK_RECORD":
raise const.DXFStructureError(
f"expected BLOCK_RECORD(#{handle}) for layout '{layout.dxf.name}' "
f"has invalid entity type: {block_record.dxftype()}"
)
# link is maybe broken
block_record.dxf.layout = layout.dxf.handle
super().__init__(block_record) # type: ignore
@classmethod
def new(cls, name: str, block_name: str, doc: Drawing, dxfattribs=None) -> Layout:
"""Returns the required structures for a new layout:
- a :class:`BlockLayout` with BLOCK_RECORD, BLOCK and ENDBLK entities
- LAYOUT entity in the objects section
Args:
name: layout name as shown in tabs of CAD applications e.g. 'Layout2'
block_name: layout block name e.g. '*Paper_Space2'
doc: drawing document
dxfattribs: additional DXF attributes for LAYOUT entity
(internal API)
"""
block_layout: BlockLayout = doc.blocks.new(block_name)
dxfattribs = dxfattribs or {}
dxfattribs.update(
{
"name": name,
"block_record_handle": block_layout.block_record_handle,
}
)
dxf_layout = doc.objects.new_entity("LAYOUT", dxfattribs=dxfattribs)
return cls(dxf_layout, doc) # type: ignore
@classmethod
def load(cls, layout: DXFLayout, doc: Drawing):
"""Loading interface. (internal API)"""
_layout = cls(layout, doc)
_layout._repair_owner_tags()
return _layout
@property
def name(self) -> str:
"""Layout name as shown in tabs of :term:`CAD` applications."""
return self.dxf_layout.dxf.name
@property # dynamic DXF attribute dispatching, e.g. DXFLayout.dxf.layout_flags
def dxf(self) -> Any:
"""Returns the DXF name space attribute of the associated
:class:`~ezdxf.entities.DXFLayout` object.
This enables direct access to the underlying LAYOUT entity,
e.g. ``Layout.dxf.layout_flags``
"""
return self.dxf_layout.dxf
@property
def block_record_name(self) -> str:
"""Returns the name of the associated BLOCK_RECORD as string."""
return self.block_record.dxf.name
def _repair_owner_tags(self) -> None:
"""Set `owner` and `paperspace` attributes of entities hosted by this
layout to correct values.
"""
layout_key = self.layout_key
paperspace = 0 if self.is_modelspace else 1
for entity in self:
if entity.dxf.owner != layout_key:
entity.dxf.owner = layout_key
if entity.dxf.paperspace != paperspace:
entity.dxf.paperspace = paperspace
def __contains__(self, entity: Union[DXFGraphic, str]) -> bool:
"""Returns ``True`` if `entity` is stored in this layout.
Args:
entity: :class:`DXFGraphic` object or handle as hex string
"""
if isinstance(entity, str): # entity is a handle string
entity = self.entitydb[entity] # type: ignore
return entity.dxf.owner == self.layout_key # type: ignore
def destroy(self) -> None:
"""Delete all entities and the layout itself from entity database and
all linked structures.
(internal API)
"""
self.doc.objects.delete_entity(self.dxf_layout)
# super() deletes block_record and associated entity space
super().destroy()
@property
def plot_layout_flags(self) -> int:
return self.dxf_layout.dxf.plot_layout_flags
def reset_extents(
self, extmin=(+1e20, +1e20, +1e20), extmax=(-1e20, -1e20, -1e20)
) -> None:
"""Reset `extents`_ to given values or the AutoCAD default values.
"Drawing extents are the bounds of the area occupied by objects."
(Quote Autodesk Knowledge Network)
Args:
extmin: minimum extents or (+1e20, +1e20, +1e20) as default value
extmax: maximum extents or (-1e20, -1e20, -1e20) as default value
"""
dxf = self.dxf_layout.dxf
dxf.extmin = extmin
dxf.extmax = extmax
def reset_limits(self, limmin=None, limmax=None) -> None:
"""Reset `limits`_ to given values or the AutoCAD default values.
"Sets an invisible rectangular boundary in the drawing area that can
limit the grid display and limit clicking or entering point locations."
(Quote Autodesk Knowledge Network)
The :class:`Paperspace` class has an additional method
:meth:`~Paperspace.reset_paper_limits` to deduce the default limits from
the paper size settings.
Args:
limmin: minimum limits or (0, 0) as default
limmax: maximum limits or (paper width, paper height) as default value
"""
dxf = self.dxf_layout.dxf
if limmin is None:
limmin = (0, 0)
if limmax is None:
limmax = (dxf.paper_width, dxf.paper_height)
dxf.limmin = limmin
dxf.limmax = limmax
def set_plot_type(self, value: int = 5) -> None:
"""
=== ============================================================
0 last screen display
1 drawing extents
2 drawing limits
3 view specific (defined by :attr:`Layout.dxf.plot_view_name`)
4 window specific (defined by :meth:`Layout.set_plot_window_limits`)
5 layout information (default)
=== ============================================================
Args:
value: plot type
Raises:
DXFValueError: for `value` out of range
"""
if 0 <= int(value) <= 5:
self.dxf.plot_type = value
else:
raise const.DXFValueError("Plot type value out of range (0-5).")
def set_plot_style(self, name: str = "ezdxf.ctb", show: bool = False) -> None:
"""Set plot style file of type `.ctb`.
Args:
name: plot style filename
show: show plot style effect in preview? (AutoCAD specific attribute)
"""
self.dxf_layout.dxf.current_style_sheet = name
self.use_plot_styles(True)
self.show_plot_styles(show)
def get_plot_style_filename(self) -> str:
return self.dxf_layout.dxf.current_style_sheet
def set_plot_window(
self,
lower_left: tuple[float, float] = (0, 0),
upper_right: tuple[float, float] = (0, 0),
) -> None:
"""Set plot window size in (scaled) paper space units.
Args:
lower_left: lower left corner as 2D point
upper_right: upper right corner as 2D point
"""
x1, y1 = lower_left
x2, y2 = upper_right
dxf = self.dxf_layout.dxf
dxf.plot_window_x1 = x1
dxf.plot_window_y1 = y1
dxf.plot_window_x2 = x2
dxf.plot_window_y2 = y2
self.set_plot_type(4)
def get_plot_unit_scale_factor(self) -> float:
denom = self.dxf.scale_denominator
numerator = self.dxf.scale_numerator
scale = 1.0
if denom:
scale = numerator / denom
if self.dxf.plot_paper_units == 1:
return scale # mm
else:
return scale * 25.4 # inch
# plot layout flags setter
def plot_viewport_borders(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_VIEWPORT_BORDERS, state)
def show_plot_styles(self, state: bool = True) -> None:
self.set_plot_flags(self.SHOW_PLOT_STYLES, state)
def plot_centered(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_CENTERED, state)
def plot_hidden(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_HIDDEN, state)
def use_standard_scale(self, state: bool = True) -> None:
self.set_plot_flags(self.USE_STANDARD_SCALE, state)
def use_plot_styles(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_PLOTSTYLES, state)
def scale_lineweights(self, state: bool = True) -> None:
self.set_plot_flags(self.SCALE_LINEWEIGHTS, state)
def print_lineweights(self, state: bool = True) -> None:
self.set_plot_flags(self.PRINT_LINEWEIGHTS, state)
def draw_viewports_first(self, state: bool = True) -> None:
self.set_plot_flags(self.DRAW_VIEWPORTS_FIRST, state)
def model_type(self, state: bool = True) -> None:
self.set_plot_flags(self.MODEL_TYPE, state)
def update_paper(self, state: bool = True) -> None:
self.set_plot_flags(self.UPDATE_PAPER, state)
def zoom_to_paper_on_update(self, state: bool = True) -> None:
self.set_plot_flags(self.ZOOM_TO_PAPER_ON_UPDATE, state)
def plot_flags_initializing(self, state: bool = True) -> None:
self.set_plot_flags(self.INITIALIZING, state)
def prev_plot_init(self, state: bool = True) -> None:
self.set_plot_flags(self.PREV_PLOT_INIT, state)
def set_plot_flags(self, flag, state: bool = True) -> None:
self.dxf_layout.set_flag_state(flag, state=state, name="plot_layout_flags")
class Modelspace(Layout):
""":class:`Modelspace` - not deletable, all entities of this layout are
stored in the ENTITIES section of the DXF file, the associated
"*Model_Space" block is empty, block name is fixed as "*Model_Space",
the name is fixed as "Model".
"""
@property
def name(self) -> str:
"""Name of modelspace is fixed as "Model"."""
return "Model"
def new_geodata(self, dxfattribs=None) -> GeoData:
"""Creates a new :class:`GeoData` entity and replaces existing ones.
The GEODATA entity resides in the OBJECTS section and not in the
modelspace, it is linked to the modelspace by an
:class:`~ezdxf.entities.ExtensionDict` located in BLOCK_RECORD of the
modelspace.
The GEODATA entity requires DXF R2010. The DXF reference does not
document if other layouts than the modelspace supports geo referencing,
so I assume getting/setting geo data may only make sense for the
modelspace.
Args:
dxfattribs: DXF attributes for :class:`~ezdxf.entities.GeoData` entity
"""
if self.doc.dxfversion < const.DXF2010:
raise const.DXFValueError("GEODATA entity requires DXF R2010 or later.")
if dxfattribs is None:
dxfattribs = {}
xdict = self.get_extension_dict()
geodata = self.doc.objects.add_geodata(
owner=xdict.dictionary.dxf.handle,
dxfattribs=dxfattribs,
)
xdict["ACAD_GEOGRAPHICDATA"] = geodata
return geodata
def get_geodata(self) -> Optional[GeoData]:
"""Returns the :class:`~ezdxf.entities.GeoData` entity associated to
the modelspace or ``None``.
"""
try:
xdict = self.block_record.get_extension_dict()
except AttributeError:
return None
try:
return xdict["ACAD_GEOGRAPHICDATA"]
except const.DXFKeyError:
return None
class Paperspace(Layout):
"""There are two kind of paperspace layouts:
1. Active Layout - all entities of this layout are stored in the ENTITIES
section, the associated "*Paper_Space" block is empty, block name
"*Paper_Space" is mandatory and also marks the active layout, the layout
name can be an arbitrary string.
2. Inactive Layout - all entities of this layouts are stored in the
associated BLOCK called "*Paper_SpaceN", where "N" is an arbitrary
number, I don't know if the block name schema "*Paper_SpaceN" is
mandatory, the layout name can be an arbitrary string.
There is no different handling for active layouts and inactive layouts in
`ezdxf`, this differentiation is just for AutoCAD important and it is not
documented in the DXF reference.
"""
def rename(self, name: str) -> None:
"""Rename layout to `name`, changes the name displayed in tabs by
CAD applications, not the internal BLOCK name. (internal API)
Use method :meth:`~ezdxf.layouts.Layouts.rename` of the
:meth:`~ezdxf.layouts.Layouts` class to rename paper space
layouts.
"""
self.dxf_layout.dxf.name = name
def viewports(self) -> list[Viewport]:
"""Get all VIEWPORT entities defined in this paperspace layout."""
return [e for e in self if e.is_alive and e.dxftype() == "VIEWPORT"] # type: ignore
def main_viewport(self) -> Optional[Viewport]:
"""Returns the main viewport of this paper space layout, or ``None``
if no main viewport exist.
"""
# Theory: the first VP found is the main VP of the layout, the attributes status
# and id are ignored by BricsCAD and AutoCAD!?
for viewport in self.viewports():
dxf = viewport.dxf
if dxf.hasattr("status") and dxf.status == 1:
return viewport
if dxf.id == 1:
return viewport
return None
def add_viewport(
self,
center: UVec,
size: tuple[float, float],
view_center_point: UVec,
view_height: float,
status: int = 2,
dxfattribs=None,
) -> Viewport:
"""Add a new :class:`~ezdxf.entities.Viewport` entity.
Viewport :attr:`status`:
- -1 is on, but is fully off-screen, or is one of the viewports that is not
active because the $MAXACTVP count is currently being exceeded.
- 0 is off
- any value>0 is on and active. The value indicates the order of
stacking for the viewports, where 1 is the "active viewport", 2 is the
next, ...
"""
dxfattribs = dxfattribs or {}
width, height = size
attribs = {
"center": center, # center in paperspace
"width": width, # width in paperspace
"height": height, # height in paperspace
"status": status,
"layer": "VIEWPORTS",
# use separated layer to turn off for plotting
"view_center_point": view_center_point, # in modelspace
"view_height": view_height, # in modelspace
}
attribs.update(dxfattribs)
viewport = cast("Viewport", self.new_entity("VIEWPORT", attribs))
viewport.dxf.id = self.get_next_viewport_id()
return viewport
def get_next_viewport_id(self):
viewports = self.viewports()
if viewports:
return max(vp.dxf.id for vp in viewports) + 1
return 2
def reset_viewports(self) -> None:
"""Delete all existing viewports, and create a new main viewport."""
# remove existing viewports
for viewport in self.viewports():
self.delete_entity(viewport)
self.add_new_main_viewport()
def reset_main_viewport(self, center: UVec = None, size: UVec = None) -> Viewport:
"""Reset the main viewport of this paper space layout to the given
values, or reset them to the default values, deduced from the paper
settings. Creates a new main viewport if none exist.
Ezdxf does not create a main viewport by default, because CAD
applications don't require one.
Args:
center: center of the viewport in paper space units
size: viewport size as (width, height) tuple in paper space units
"""
viewport = self.main_viewport()
if viewport is None:
viewport = self.add_new_main_viewport()
default_center, default_size = self.default_viewport_config()
if center is None:
center = default_center
if size is None:
size = default_size
viewport.dxf.center = center
width, height = size
viewport.dxf.width = width
viewport.dxf.height = height
return viewport
def default_viewport_config(
self,
) -> tuple[tuple[float, float], tuple[float, float]]:
dxf = self.dxf_layout.dxf
if dxf.plot_paper_units == 0: # inches
unit_factor = 25.4
else: # mm
unit_factor = 1.0
# all paper parameters in mm!
# all viewport parameters in paper space units inch/mm + scale factor!
scale_factor = dxf.scale_denominator / dxf.scale_numerator
def paper_units(value):
return value / unit_factor * scale_factor
paper_width = paper_units(dxf.paper_width)
paper_height = paper_units(dxf.paper_height)
# plot origin offset
x_offset = paper_units(dxf.plot_origin_x_offset)
y_offset = paper_units(dxf.plot_origin_y_offset)
# printing area
printable_width = (
paper_width - paper_units(dxf.left_margin) - paper_units(dxf.right_margin)
)
printable_height = (
paper_height - paper_units(dxf.bottom_margin) - paper_units(dxf.top_margin)
)
# AutoCAD viewport (window) size
vp_width = paper_width * 1.1
vp_height = paper_height * 1.1
# center of printing area
center = (
printable_width / 2 - x_offset,
printable_height / 2 - y_offset,
)
return center, (vp_width, vp_height)
def add_new_main_viewport(self) -> Viewport:
"""Add a new main viewport."""
center, size = self.default_viewport_config()
vp_height = size[1]
# create 'main' viewport
main_viewport = self.add_viewport(
center=center, # no influence to 'main' viewport?
size=size, # I don't get it, just use paper size!
view_center_point=center, # same as center
view_height=vp_height, # view height in paper space units
status=1, # main viewport
)
if len(self.entity_space) > 1:
# move main viewport to index 0 of entity space
_vp = self.entity_space.pop()
assert _vp is main_viewport
self.entity_space.insert(0, main_viewport)
main_viewport.dxf.id = 1 # set as main viewport
main_viewport.dxf.flags = 557088 # AutoCAD default value
self.set_current_viewport_handle(main_viewport.dxf.handle)
return main_viewport
def set_current_viewport_handle(self, handle: str) -> None:
self.dxf_layout.dxf.viewport_handle = handle
def page_setup(
self,
size: tuple[float, float] = (297, 210),
margins: tuple[float, float, float, float] = (0, 0, 0, 0),
units: str = "mm",
offset: tuple[float, float] = (0, 0),
rotation: int = 0,
scale: Union[int, tuple[float, float]] = 16,
name: str = "ezdxf",
device: str = "DWG to PDF.pc3",
) -> None:
"""Setup plot settings and paper size and reset viewports.
All parameters in given `units` (mm or inch).
Reset paper limits, extents and viewports.
Args:
size: paper size as (width, height) tuple
margins: (top, right, bottom, left) hint: clockwise
units: "mm" or "inch"
offset: plot origin offset is 2D point
rotation: see table Rotation
scale: integer in range [0, 32] defines a standard scale type or
as tuple(numerator, denominator) e.g. (1, 50) for scale 1:50
name: paper name prefix "{name}_({width}_x_{height}_{unit})"
device: device .pc3 configuration file or system printer name
=== ============
int Rotation
=== ============
0 no rotation
1 90 degrees counter-clockwise
2 upside-down
3 90 degrees clockwise
=== ============
"""
if int(rotation) not in (0, 1, 2, 3):
raise const.DXFValueError("valid rotation values: 0-3")
if isinstance(scale, tuple):
standard_scale = 16
scale_num, scale_denom = scale
elif isinstance(scale, int):
standard_scale = scale
scale_num, scale_denom = const.STD_SCALES.get(standard_scale, (1.0, 1.0))
else:
raise const.DXFTypeError(
"Scale has to be an int or a tuple(numerator, denominator)"
)
if scale_num == 0:
raise const.DXFValueError("Scale numerator can't be 0.")
if scale_denom == 0:
raise const.DXFValueError("Scale denominator can't be 0.")
paper_width, paper_height = size
margin_top, margin_right, margin_bottom, margin_left = margins
units = units.lower()
if units.startswith("inch"):
units = "Inches"
plot_paper_units = 0
unit_factor = 25.4 # inch to mm
elif units == "mm":
units = "MM"
plot_paper_units = 1
unit_factor = 1.0
else:
raise const.DXFValueError('Supported units: "mm" and "inch"')
# Setup PLOTSETTINGS
# all paper sizes in mm
dxf = self.dxf_layout.dxf
if not dxf.hasattr("plot_layout_flags"):
dxf.plot_layout_flags = dxf.get_default("plot_layout_flags")
self.use_standard_scale(False) # works best, don't know why
dxf.page_setup_name = ""
dxf.plot_configuration_file = device
dxf.paper_size = f"{name}_({paper_width:.2f}_x_{paper_height:.2f}_{units})"
dxf.left_margin = margin_left * unit_factor
dxf.bottom_margin = margin_bottom * unit_factor
dxf.right_margin = margin_right * unit_factor
dxf.top_margin = margin_top * unit_factor
dxf.paper_width = paper_width * unit_factor
dxf.paper_height = paper_height * unit_factor
dxf.scale_numerator = scale_num
dxf.scale_denominator = scale_denom
dxf.plot_paper_units = plot_paper_units
dxf.plot_rotation = rotation
x_offset, y_offset = offset
dxf.plot_origin_x_offset = x_offset * unit_factor # conversion to mm
dxf.plot_origin_y_offset = y_offset * unit_factor # conversion to mm
dxf.standard_scale_type = standard_scale
dxf.unit_factor = 1.0 / unit_factor # 1/1 for mm; 1/25.4 ... for inch
# Setup Layout
self.reset_paper_limits()
self.reset_extents()
self.reset_viewports()
def reset_paper_limits(self) -> None:
"""Set paper limits to default values, all values in paperspace units
but without plot scale (?).
"""
dxf = self.dxf_layout.dxf
if dxf.plot_paper_units == 0: # inch
unit_factor = 25.4
else: # mm
unit_factor = 1.0
# all paper sizes are stored in mm
paper_width = dxf.paper_width / unit_factor # in plot paper units
paper_height = dxf.paper_height / unit_factor # in plot paper units
left_margin = dxf.left_margin / unit_factor
bottom_margin = dxf.bottom_margin / unit_factor
x_offset = dxf.plot_origin_x_offset / unit_factor
y_offset = dxf.plot_origin_y_offset / unit_factor
# plot origin is the lower left corner of the printable paper area
# limits are the paper borders relative to the plot origin
shift_x = left_margin + x_offset
shift_y = bottom_margin + y_offset
dxf.limmin = (-shift_x, -shift_y) # paper space units
dxf.limmax = (paper_width - shift_x, paper_height - shift_y)
def get_paper_limits(self) -> tuple[Vec2, Vec2]:
"""Returns paper limits in plot paper units, relative to the plot origin.
plot origin = lower left corner of printable area + plot origin offset
Returns:
tuple (Vec2(x1, y1), Vec2(x2, y2)), lower left corner is (x1, y1),
upper right corner is (x2, y2).
"""
return Vec2(self.dxf.limmin), Vec2(self.dxf.limmax)
def page_setup_r12(
self,
size: tuple[float, float] = (297, 210),
margins: tuple[float, float, float, float] = (0, 0, 0, 0),
units: str = "mm",
offset: tuple[float, float] = (0, 0),
rotation: float = 0,
scale: Union[int, tuple[float, float]] = 16,
) -> None:
# remove existing viewports
for viewport in self.viewports():
self.delete_entity(viewport)
if int(rotation) not in (0, 1, 2, 3):
raise const.DXFValueError("Valid rotation values: 0-3")
if isinstance(scale, int):
scale_num, scale_denom = const.STD_SCALES.get(scale, (1, 1))
else:
scale_num, scale_denom = scale
if scale_num == 0:
raise const.DXFValueError("Scale numerator can't be 0.")
if scale_denom == 0:
raise const.DXFValueError("Scale denominator can't be 0.")
scale_factor = scale_denom / scale_num
# TODO: don't know how to set inch or mm mode in R12
units = units.lower()
if units.startswith("inch"):
units = "Inches"
plot_paper_units = 0
unit_factor = 25.4 # inch to mm
elif units == "mm":
units = "MM"
plot_paper_units = 1
unit_factor = 1.0
else:
raise const.DXFValueError('Supported units: "mm" and "inch"')
# all viewport parameters are scaled paper space units
def paper_units(value):
return value * scale_factor
margin_top = paper_units(margins[0])
margin_right = paper_units(margins[1])
margin_bottom = paper_units(margins[2])
margin_left = paper_units(margins[3])
paper_width = paper_units(size[0])
paper_height = paper_units(size[1])
self.doc.header["$PLIMMIN"] = (0, 0)
self.doc.header["$PLIMMAX"] = (paper_width, paper_height)
self.doc.header["$PEXTMIN"] = (0, 0, 0)
self.doc.header["$PEXTMAX"] = (paper_width, paper_height, 0)
# printing area
printable_width = paper_width - margin_left - margin_right
printable_height = paper_height - margin_bottom - margin_top
# AutoCAD viewport (window) size
vp_width = paper_width * 1.1
vp_height = paper_height * 1.1
# center of printing area
center = (printable_width / 2, printable_height / 2)
# create 'main' viewport
main_viewport = self.add_viewport(
center=center, # no influence to 'main' viewport?
size=(vp_width, vp_height), # I don't get it, just use paper size!
view_center_point=center, # same as center
view_height=vp_height, # view height in paper space units
status=1, # main viewport
)
main_viewport.dxf.id = 1 # set as main viewport
main_viewport.dxf.render_mode = 1000 # AutoDesk default (view mode?)
def get_paper_limits_r12(self) -> tuple[Vec2, Vec2]:
"""Returns paper limits in plot paper units."""
limmin = self.doc.header.get("$PLIMMIN", (0, 0))
limmax = self.doc.header.get("$PLIMMAX", (0, 0))
return Vec2(limmin), Vec2(limmax)
def _find_layout_block_record(layout: DXFLayout) -> BlockRecord | None:
"""Find and link the lost BLOCK_RECORD for the given LAYOUT entity."""
def link_layout(block_record: BlockRecord) -> None:
logger.info(
f"fixing broken links for '{layout.dxf.name}' between {str(layout)} and {str(block_record)}"
)
layout.dxf.block_record_handle = block_record.dxf.handle
block_record.dxf.layout = layout.dxf.handle
doc = layout.doc
assert doc is not None
if layout.dxf.name == "Model": # modelspace layout
key = make_table_key("*Model_Space")
for block_record in doc.tables.block_records:
if make_table_key(block_record.dxf.name) == key:
link_layout(block_record)
return block_record
return None
# paperspace layout
search_key = make_table_key("*Paper_Space")
paper_space_records = [
block_record
for block_record in doc.tables.block_records
if make_table_key(block_record.dxf.name).startswith(search_key)
]
def search(key):
for block_record in paper_space_records:
layout_handle = block_record.dxf.get("layout", "0")
if doc.entitydb.get(layout_handle) is key:
link_layout(block_record)
return block_record
return None
# first search all records for the lost record
block_record = search(layout)
if block_record is None:
# link layout to the next orphaned record
block_record = search(None)
return block_record

View File

@@ -0,0 +1,384 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterator, cast, Optional
import logging
from ezdxf.lldxf.const import DXFKeyError, DXFValueError, DXFInternalEzdxfError
from ezdxf.lldxf.const import (
MODEL_SPACE_R2000,
PAPER_SPACE_R2000,
TMP_PAPER_SPACE_NAME,
)
from ezdxf.lldxf.validator import is_valid_table_name
from .layout import Layout, Modelspace, Paperspace
from ezdxf.entities import DXFEntity, BlockRecord
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.document import Drawing
from ezdxf.entities import Dictionary, DXFLayout
logger = logging.getLogger("ezdxf")
def key(name: str) -> str:
"""AutoCAD uses case insensitive layout names, but stores the name case
sensitive."""
return name.upper()
MODEL = key("Model")
class Layouts:
def __init__(self, doc: Drawing):
"""Default constructor. (internal API)"""
self.doc = doc
# Store layout names in normalized form: key(name)
self._layouts: dict[str, Layout] = {}
# key: layout name as original case-sensitive string; value: DXFLayout()
self._dxf_layouts: Dictionary = cast(
"Dictionary", self.doc.rootdict["ACAD_LAYOUT"]
)
@classmethod
def setup(cls, doc: Drawing):
"""Constructor from scratch. (internal API)"""
layouts = Layouts(doc)
layouts.setup_modelspace()
layouts.setup_paperspace()
return layouts
def __len__(self) -> int:
"""Returns count of existing layouts, including the modelspace
layout."""
return len(self._layouts)
def __contains__(self, name: str) -> bool:
"""Returns ``True`` if layout `name` exist."""
assert isinstance(name, str), type(str)
return key(name) in self._layouts
def __iter__(self) -> Iterator[Layout]:
"""Returns iterable of all layouts as :class:`~ezdxf.layouts.Layout`
objects, including the modelspace layout.
"""
return iter(self._layouts.values())
def _add_layout(self, name: str, layout: Layout):
dxf_layout = layout.dxf_layout
dxf_layout.dxf.name = name
dxf_layout.dxf.owner = self._dxf_layouts.dxf.handle
self._layouts[key(name)] = layout
self._dxf_layouts[name] = dxf_layout
def _discard(self, layout: Layout):
name = layout.name
self._dxf_layouts.discard(name)
del self._layouts[key(name)]
def append_layout(self, layout: Layout) -> None:
"""Append an existing (copied) paperspace layout as last layout tab."""
index = 1
base_layout_name = layout.dxf.name
layout_name = base_layout_name
while layout_name in self:
index += 1
layout_name = base_layout_name + f" ({index})"
layout.dxf.taborder = len(self._layouts) + 1
self._add_layout(layout_name, layout)
def setup_modelspace(self):
"""Modelspace setup. (internal API)"""
self._new_special(
Modelspace, "Model", MODEL_SPACE_R2000, dxfattribs={"taborder": 0}
)
def setup_paperspace(self):
"""First layout setup. (internal API)"""
self._new_special(
Paperspace, "Layout1", PAPER_SPACE_R2000, dxfattribs={"taborder": 1}
)
def _new_special(self, cls, name: str, block_name: str, dxfattribs: dict) -> Layout:
if name in self._layouts:
raise DXFValueError(f'Layout "{name}" already exists')
dxfattribs["owner"] = self._dxf_layouts.dxf.handle
layout = cls.new(name, block_name, self.doc, dxfattribs=dxfattribs)
self._add_layout(name, layout)
return layout
def unique_paperspace_name(self) -> str:
"""Returns a unique paperspace name. (internal API)"""
blocks = self.doc.blocks
count = 0
while "*Paper_Space%d" % count in blocks:
count += 1
return "*Paper_Space%d" % count
def new(self, name: str, dxfattribs=None) -> Paperspace:
"""Returns a new :class:`~ezdxf.layouts.Paperspace` layout.
Args:
name: layout name as shown in tabs in :term:`CAD` applications
dxfattribs: additional DXF attributes for the
:class:`~ezdxf.entities.layout.DXFLayout` entity
Raises:
DXFValueError: Invalid characters in layout name.
DXFValueError: Layout `name` already exist.
"""
assert isinstance(name, str), type(str)
if not is_valid_table_name(name):
raise DXFValueError("Layout name contains invalid characters.")
if name in self:
raise DXFValueError(f'Layout "{name}" already exist.')
dxfattribs = dict(dxfattribs or {}) # copy attribs
dxfattribs["owner"] = self._dxf_layouts.dxf.handle
dxfattribs.setdefault("taborder", len(self._layouts) + 1)
block_name = self.unique_paperspace_name()
layout = Paperspace.new(name, block_name, self.doc, dxfattribs=dxfattribs)
# Default extents are ok!
# Reset limits to (0, 0) and (paper width, paper height)
layout.reset_limits()
self._add_layout(name, layout)
return layout # type: ignore
@classmethod
def load(cls, doc: "Drawing") -> "Layouts":
"""Constructor if loading from file. (internal API)"""
layouts = cls(doc)
layouts.setup_from_rootdict()
# DXF R12: block/block_record for *Model_Space and *Paper_Space
# already exist:
if len(layouts) < 2: # restore missing DXF Layouts
layouts.restore("Model", MODEL_SPACE_R2000, taborder=0)
layouts.restore("Layout1", PAPER_SPACE_R2000, taborder=1)
return layouts
def restore(self, name: str, block_record_name: str, taborder: int) -> None:
"""Restore layout from block if DXFLayout does not exist.
(internal API)"""
if name in self:
return
block_layout = self.doc.blocks.get(block_record_name)
self._new_from_block_layout(name, block_layout, taborder)
def _new_from_block_layout(self, name, block_layout, taborder: int) -> "Layout":
dxfattribs = {
"owner": self._dxf_layouts.dxf.handle,
"name": name,
"block_record_handle": block_layout.block_record_handle,
"taborder": taborder,
}
dxf_layout = cast(
"DXFLayout",
self.doc.objects.new_entity("LAYOUT", dxfattribs=dxfattribs),
)
if key(name) == MODEL:
layout = Modelspace.load(dxf_layout, self.doc)
else:
layout = Paperspace.load(dxf_layout, self.doc)
self._add_layout(name, layout)
return layout
def setup_from_rootdict(self) -> None:
"""Setup layout manager from root dictionary. (internal API)"""
layout: Layout
for name, dxf_layout in self._dxf_layouts.items():
if isinstance(dxf_layout, str):
logger.debug(f"ignore missing LAYOUT(#{dxf_layout}) entity '{name}'")
continue
if key(name) == MODEL:
layout = Modelspace(dxf_layout, self.doc)
else:
layout = Paperspace(dxf_layout, self.doc)
# assert name == layout.dxf.name
self._layouts[key(name)] = layout
def modelspace(self) -> Modelspace:
"""Returns the :class:`~ezdxf.layouts.Modelspace` layout."""
return cast(Modelspace, self.get("Model"))
def names(self) -> list[str]:
"""Returns a list of all layout names, all names in original case
sensitive form."""
return [layout.name for layout in self._layouts.values()]
def get(self, name: Optional[str]) -> Layout:
"""Returns :class:`~ezdxf.layouts.Layout` by `name`, case insensitive
"Model" == "MODEL".
Args:
name: layout name as shown in tab, e.g. ``'Model'`` for modelspace
"""
name = name or self.names_in_taborder()[1] # first paperspace layout
return self._layouts[key(name)]
def rename(self, old_name: str, new_name: str) -> None:
"""Rename a layout from `old_name` to `new_name`.
Can not rename layout ``'Model'`` and the new name of a layout must
not exist.
Args:
old_name: actual layout name, case insensitive
new_name: new layout name, case insensitive
Raises:
DXFValueError: try to rename ``'Model'``
DXFValueError: Layout `new_name` already exist.
"""
assert isinstance(old_name, str), type(old_name)
assert isinstance(new_name, str), type(new_name)
if key(old_name) == MODEL:
raise DXFValueError("Can not rename model space.")
if new_name in self:
raise DXFValueError(f'Layout "{new_name}" already exist.')
if old_name not in self:
raise DXFValueError(f'Layout "{old_name}" does not exist.')
layout = self.get(old_name)
self._discard(layout)
layout.rename(new_name)
self._add_layout(new_name, layout)
def names_in_taborder(self) -> list[str]:
"""Returns all layout names in tab order as shown in :term:`CAD`
applications."""
names = [
(layout.dxf.taborder, layout.name) for layout in self._layouts.values()
]
return [name for order, name in sorted(names)]
def get_layout_for_entity(self, entity: DXFEntity) -> Layout:
"""Returns the owner layout for a DXF `entity`."""
owner = entity.dxf.owner
if owner is None:
raise DXFKeyError("No associated layout, owner is None.")
return self.get_layout_by_key(entity.dxf.owner)
def get_layout_by_key(self, layout_key: str) -> Layout:
"""Returns a layout by its `layout_key`. (internal API)"""
assert isinstance(layout_key, str), type(layout_key)
error_msg = f'Layout with key "{layout_key}" does not exist.'
try:
block_record = self.doc.entitydb[layout_key]
except KeyError:
raise DXFKeyError(error_msg)
if not isinstance(block_record, BlockRecord):
# VERTEX, ATTRIB, SEQEND are owned by the parent entity
raise DXFKeyError(error_msg)
try:
dxf_layout = self.doc.entitydb[block_record.dxf.layout]
except KeyError:
raise DXFKeyError(error_msg)
return self.get(dxf_layout.dxf.name)
def get_active_layout_key(self):
"""Returns layout kay for the active paperspace layout.
(internal API)"""
active_layout_block_record = self.doc.block_records.get(PAPER_SPACE_R2000)
return active_layout_block_record.dxf.handle
def set_active_layout(self, name: str) -> None:
"""Set layout `name` as active paperspace layout."""
assert isinstance(name, str), type(name)
if key(name) == MODEL: # reserved layout name
raise DXFValueError("Can not set model space as active layout")
# raises KeyError if layout 'name' does not exist
new_active_layout = self.get(name)
old_active_layout_key = self.get_active_layout_key()
if old_active_layout_key == new_active_layout.layout_key:
return # layout 'name' is already the active layout
blocks = self.doc.blocks
new_active_paper_space_name = new_active_layout.block_record_name
blocks.rename_block(PAPER_SPACE_R2000, TMP_PAPER_SPACE_NAME)
blocks.rename_block(new_active_paper_space_name, PAPER_SPACE_R2000)
blocks.rename_block(TMP_PAPER_SPACE_NAME, new_active_paper_space_name)
def delete(self, name: str) -> None:
"""Delete layout `name` and destroy all entities in that layout.
Args:
name (str): layout name as shown in tabs
Raises:
DXFKeyError: if layout `name` do not exists
DXFValueError: deleting modelspace layout is not possible
DXFValueError: deleting last paperspace layout is not possible
"""
assert isinstance(name, str), type(name)
if key(name) == MODEL:
raise DXFValueError("Can not delete modelspace layout.")
layout = self.get(name)
if len(self) < 3:
raise DXFValueError("Can not delete last paperspace layout.")
if layout.layout_key == self.get_active_layout_key():
# Layout `name` is the active layout:
for layout_name in self._layouts:
# Set any other paperspace layout as active layout
if layout_name not in (key(name), MODEL):
self.set_active_layout(layout_name)
break
self._discard(layout)
layout.destroy()
def active_layout(self) -> Paperspace:
"""Returns the active paperspace layout."""
for layout in self:
if layout.is_active_paperspace:
return cast(Paperspace, layout)
raise DXFInternalEzdxfError("No active paperspace layout found.")
def audit(self, auditor: Auditor):
from ezdxf.audit import AuditError
doc = auditor.doc
# Find/remove orphaned LAYOUT objects:
layouts = (o for o in doc.objects if o.dxftype() == "LAYOUT")
for layout in layouts:
name = layout.dxf.get("name")
if name not in self:
auditor.fixed_error(
code=AuditError.ORPHANED_LAYOUT_ENTITY,
message=f'Removed orphaned {str(layout)} "{name}"',
)
doc.objects.delete_entity(layout)
# Find/remove orphaned paperspace BLOCK_RECORDS named: *Paper_Space...
psp_br_handles = {
br.dxf.handle
for br in doc.block_records
if br.dxf.name.lower().startswith("*paper_space")
}
psp_layout_br_handles = {
layout.dxf.block_record_handle
for layout in self._layouts.values()
if key(layout.name) != MODEL
}
mismatch = psp_br_handles.difference(psp_layout_br_handles)
if len(mismatch):
for handle in mismatch:
br = doc.entitydb.get(handle)
name = br.dxf.get("name") # type: ignore
auditor.fixed_error(
code=AuditError.ORPHANED_PAPER_SPACE_BLOCK_RECORD_ENTITY,
message=f'Removed orphaned layout {str(br)} "{name}"',
)
if name in doc.blocks:
doc.blocks.delete_block(name)
else:
doc.block_records.remove(name)
# Does not check the LAYOUT content this is done in the BlockSection,
# because the content of layouts is stored as blocks.