Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/entitydb.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

433 lines
16 KiB
Python

# Copyright (c) 2019-2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
Optional,
Iterable,
TYPE_CHECKING,
Iterator,
)
from contextlib import contextmanager
from ezdxf.tools.handle import HandleGenerator
from ezdxf.lldxf.types import is_valid_handle
from ezdxf.entities.dxfentity import DXFEntity
from ezdxf.entities.dxfobj import DXFObject
from ezdxf.audit import AuditError, Auditor
from ezdxf.lldxf.const import DXFInternalEzdxfError
from ezdxf.entities import factory
from ezdxf.query import EntityQuery
from ezdxf.entities.copy import default_copy
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
DATABASE_EXCLUDE = {
"SECTION",
"ENDSEC",
"EOF",
"TABLE",
"ENDTAB",
"CLASS",
"ACDSRECORD",
"ACDSSCHEMA",
}
class EntityDB:
"""A simple key/entity database.
Every entity/object, except tables and sections, are represented as
DXFEntity or inherited types, these entities are stored in the
DXF document database, database-key is the `handle` as string.
"""
class Trashcan:
"""Store handles to entities which should be deleted later."""
def __init__(self, db: EntityDB):
self._database = db._database
self._handles: set[str] = set()
def add(self, handle: str):
"""Put handle into trashcan to delete the entity later, this is
required for deleting entities while iterating the database.
"""
self._handles.add(handle)
def clear(self):
"""Remove handles in trashcan from database and destroy entities if
still alive.
"""
db = self._database
for handle in self._handles:
entity = db.get(handle)
if entity and entity.is_alive:
entity.destroy()
if handle in db:
del db[handle]
self._handles.clear()
def __init__(self) -> None:
self._database: dict[str, DXFEntity] = {}
# DXF handles of entities to delete later:
self.handles = HandleGenerator()
self.locked: bool = False # used only for debugging
def __getitem__(self, handle: str) -> DXFEntity:
"""Get entity by `handle`, does not filter destroyed entities nor
entities in the trashcan.
"""
return self._database[handle]
def __setitem__(self, handle: str, entity: DXFEntity) -> None:
"""Set `entity` for `handle`."""
assert isinstance(handle, str), type(handle)
assert isinstance(entity, DXFEntity), type(entity)
assert entity.is_alive, "Can not store destroyed entity."
if self.locked:
raise DXFInternalEzdxfError("Locked entity database.")
if handle == "0" or not is_valid_handle(handle):
raise ValueError(f"Invalid handle {handle}.")
self._database[handle] = entity
def __delitem__(self, handle: str) -> None:
"""Delete entity by `handle`. Removes entity only from database, does
not destroy the entity.
"""
if self.locked:
raise DXFInternalEzdxfError("Locked entity database.")
del self._database[handle]
def __contains__(self, handle: str) -> bool:
"""``True`` if database contains `handle`."""
if handle is None:
return False
assert isinstance(handle, str), type(handle)
return handle in self._database
def __len__(self) -> int:
"""Count of database items."""
return len(self._database)
def __iter__(self) -> Iterator[str]:
"""Iterable of all handles, does filter destroyed entities but not
entities in the trashcan.
"""
return self.keys() # type: ignore
def get(self, handle: str) -> Optional[DXFEntity]:
"""Returns entity for `handle` or ``None`` if no entry exist, does
not filter destroyed entities.
"""
return self._database.get(handle)
def next_handle(self) -> str:
"""Returns next unique handle."""
while True:
handle = self.handles.next()
if handle not in self._database:
return handle
def keys(self) -> Iterable[str]:
"""Iterable of all handles, does filter destroyed entities."""
return (handle for handle, entity in self.items())
def values(self) -> Iterable[DXFEntity]:
"""Iterable of all entities, does filter destroyed entities."""
return (entity for handle, entity in self.items())
def items(self) -> Iterable[tuple[str, DXFEntity]]:
"""Iterable of all (handle, entities) pairs, does filter destroyed
entities.
"""
return (
(handle, entity)
for handle, entity in self._database.items()
if entity.is_alive
)
def add(self, entity: DXFEntity) -> None:
"""Add `entity` to database, assigns a new handle to the `entity`
if :attr:`entity.dxf.handle` is ``None``. Adding the same entity
multiple times is possible and creates only a single database entry.
"""
if entity.dxftype() in DATABASE_EXCLUDE:
if entity.dxf.handle is not None:
# Mark existing entity handle as used to avoid
# reassigning the same handle again.
self[entity.dxf.handle] = entity
return
handle: str = entity.dxf.handle
if handle is None:
handle = self.next_handle()
entity.update_handle(handle)
self[handle] = entity
# Add sub entities ATTRIB, VERTEX and SEQEND to database:
# Add linked MTEXT columns to database:
if hasattr(entity, "add_sub_entities_to_entitydb"):
entity.add_sub_entities_to_entitydb(self)
def delete_entity(self, entity: DXFEntity) -> None:
"""Remove `entity` from database and destroy the `entity`."""
if entity.is_alive:
del self[entity.dxf.handle]
entity.destroy()
def discard(self, entity: DXFEntity) -> None:
"""Discard `entity` from database without destroying the `entity`."""
if entity.is_alive:
if hasattr(entity, "process_sub_entities"):
entity.process_sub_entities(lambda e: self.discard(e))
handle = entity.dxf.handle
try:
del self._database[handle]
entity.dxf.handle = None
except KeyError:
pass
def duplicate_entity(self, entity: DXFEntity) -> DXFEntity:
"""Duplicates `entity` and its sub entities (VERTEX, ATTRIB, SEQEND)
and store them with new handles in the entity database.
Graphical entities have to be added to a layout by
:meth:`~ezdxf.layouts.BaseLayout.add_entity`. DXF objects will
automatically added to the OBJECTS section.
A new owner handle will be set by adding the duplicated entity to a
layout.
Raises:
CopyNotSupported: copying of `entity` is not supported
"""
doc = entity.doc
assert doc is not None, "valid DXF document required"
new_handle = self.next_handle()
new_entity: DXFEntity = entity.copy(copy_strategy=default_copy)
new_entity.dxf.handle = new_handle
factory.bind(new_entity, doc)
if isinstance(new_entity, DXFObject):
# add DXF objects automatically to the OBJECTS section
doc.objects.add_object(new_entity)
return new_entity
def audit(self, auditor: Auditor):
"""Restore database integrity:
- restore database entries with modified handles (key != entity.dxf.handle)
- remove entities with invalid handles
- empty trashcan - destroy all entities in the trashcan
- removes destroyed database entries (purge)
"""
assert self.locked is False, "Database is locked!"
add_entities = []
with self.trashcan() as trash: # type: ignore
for handle, entity in self.items():
# Destroyed entities are already filtered!
if not is_valid_handle(handle):
auditor.fixed_error(
code=AuditError.INVALID_ENTITY_HANDLE,
message=f"Removed entity {entity.dxftype()} with invalid "
f'handle "{handle}" from entity database.',
)
trash.add(handle)
if handle != entity.dxf.get("handle"):
# database handle != stored entity handle
# prevent entity from being destroyed:
self._database[handle] = None # type: ignore
trash.add(handle)
add_entities.append(entity)
# Remove all destroyed entities from database:
self.purge()
for entity in add_entities:
handle = entity.dxf.get("handle")
if handle is None:
auditor.fixed_error(
code=AuditError.INVALID_ENTITY_HANDLE,
message=f"Removed entity {entity.dxftype()} without handle "
f"from entity database.",
)
continue
if not is_valid_handle(handle) or handle == "0":
auditor.fixed_error(
code=AuditError.INVALID_ENTITY_HANDLE,
message=f"Removed entity {entity.dxftype()} with invalid "
f'handle "{handle}" from entity database.',
)
continue
self[handle] = entity
def new_trashcan(self) -> EntityDB.Trashcan:
"""Returns a new trashcan, empty trashcan manually by: :
func:`Trashcan.clear()`.
"""
return EntityDB.Trashcan(self)
@contextmanager # type: ignore
def trashcan(self) -> EntityDB.Trashcan: # type: ignore
"""Returns a new trashcan in context manager mode, trashcan will be
emptied when leaving context.
"""
trashcan_ = self.new_trashcan()
yield trashcan_
# try ... finally is not required, in case of an exception the database
# is maybe already in an unreliable state.
trashcan_.clear()
def purge(self) -> None:
"""Remove all destroyed entities from database, but does not empty the
trashcan.
"""
# Important: operate on underlying data structure:
db = self._database
dead_handles = [handle for handle, entity in db.items() if not entity.is_alive]
for handle in dead_handles:
del db[handle]
def dxf_types_in_use(self) -> set[str]:
return set(entity.dxftype() for entity in self.values())
def reset_handle(self, entity: DXFEntity, handle: str) -> bool:
"""Try to reset the entity handle to a certain value.
Returns ``True`` if successful and ``False`` otherwise.
"""
if handle in self._database:
return False
self.discard(entity)
entity.dxf.handle = handle
self.add(entity)
return True
def query(self, query: str = "*") -> EntityQuery:
"""Entity query over all entities in the DXF document.
Args:
query: query string
.. seealso::
:ref:`entity query string` and :ref:`entity queries`
"""
return EntityQuery((e for e in self._database.values() if e.is_alive), query)
class EntitySpace:
"""
An :class:`EntitySpace` is a collection of :class:`~ezdxf.entities.DXFEntity`
objects, that stores only references to :class:`DXFEntity` objects.
The :class:`~ezdxf.layouts.Modelspace`, any :class:`~ezdxf.layouts.Paperspace`
layout and :class:`~ezdxf.layouts.BlockLayout` objects have an
:class:`EntitySpace` container to store their entities.
"""
def __init__(self, entities: Optional[Iterable[DXFEntity]] = None):
self.entities: list[DXFEntity] = (
list(e for e in entities if e.is_alive) if entities else []
)
def __iter__(self) -> Iterator[DXFEntity]:
"""Iterable of all entities, filters destroyed entities."""
return (e for e in self.entities if e.is_alive)
def __getitem__(self, index) -> DXFEntity:
"""Get entity at index `item`
:class:`EntitySpace` has a standard Python list like interface,
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 fewer entities as
``list[DXFEntity]``. Does not filter destroyed entities.
"""
return self.entities[index]
def __len__(self) -> int:
"""Count of entities including destroyed entities."""
return len(self.entities)
def has_handle(self, handle: str) -> bool:
"""``True`` if `handle` is present, does filter destroyed entities."""
assert isinstance(handle, str), type(handle)
return any(e.dxf.handle == handle for e in self)
def purge(self):
"""Remove all destroyed entities from entity space."""
self.entities = list(self)
def add(self, entity: DXFEntity) -> None:
"""Add `entity`."""
assert isinstance(entity, DXFEntity), type(entity)
assert entity.is_alive, "Can not store destroyed entities"
self.entities.append(entity)
def extend(self, entities: Iterable[DXFEntity]) -> None:
"""Add multiple `entities`."""
for entity in entities:
self.add(entity)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
"""Export all entities into DXF file by `tagwriter`.
(internal API)
"""
for entity in iter(self):
entity.export_dxf(tagwriter)
def remove(self, entity: DXFEntity) -> None:
"""Remove `entity`."""
self.entities.remove(entity)
def clear(self) -> None:
"""Remove all entities."""
# Do not destroy entities!
self.entities = list()
def pop(self, index: int = -1) -> DXFEntity:
return self.entities.pop(index)
def insert(self, index: int, entity: DXFEntity) -> None:
self.entities.insert(index, entity)
def audit(self, auditor: Auditor) -> None:
db_get = auditor.entitydb.get
purge: list[DXFEntity] = []
# Check if every entity is the entity that is stored for this handle in the
# entity database.
for entity in self:
handle = entity.dxf.handle
if entity is not db_get(handle):
# A different entity is stored in the database for this handle,
# see issues #604 and #833:
# - document has entities without handles (invalid for DXF R13+)
# - $HANDSEED is not the next usable handle
# - entity gets an already used handle
# - entity overwrites existing entity or will be overwritten by an entity
# loaded afterwards
auditor.fixed_error(
AuditError.REMOVED_INVALID_DXF_OBJECT,
f"Removed entity {entity} with a conflicting handle and without a "
f"database entry.",
)
purge.append(entity)
if not purge:
return
for entity in purge:
self.entities.remove(entity)
# These are invalid entities do not call destroy() on them, because
# this method relies on well-defined entities!
entity._silent_kill()