initial
This commit is contained in:
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
468
.venv/lib/python3.12/site-packages/ezdxf/layouts/base.py
Normal file
468
.venv/lib/python3.12/site-packages/ezdxf/layouts/base.py
Normal 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()
|
||||
132
.venv/lib/python3.12/site-packages/ezdxf/layouts/blocklayout.py
Normal file
132
.venv/lib/python3.12/site-packages/ezdxf/layouts/blocklayout.py
Normal 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)
|
||||
862
.venv/lib/python3.12/site-packages/ezdxf/layouts/layout.py
Normal file
862
.venv/lib/python3.12/site-packages/ezdxf/layouts/layout.py
Normal 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
|
||||
384
.venv/lib/python3.12/site-packages/ezdxf/layouts/layouts.py
Normal file
384
.venv/lib/python3.12/site-packages/ezdxf/layouts/layouts.py
Normal 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.
|
||||
Reference in New Issue
Block a user