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

401 lines
13 KiB
Python

# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Union, Any, Optional
from typing_extensions import Self, TypeGuard
import logging
import array
from ezdxf.lldxf import validator
from ezdxf.lldxf.const import DXF2000, DXFStructureError, SUBCLASS_MARKER
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.types import dxftag, DXFTag, DXFBinaryTag
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.tools import take2
from .dxfentity import DXFEntity, base_class, SubclassProcessor, DXFTagStorage
from .factory import register_entity
from .copy import default_copy
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = [
"DXFObject",
"Placeholder",
"XRecord",
"VBAProject",
"SortEntsTable",
"Field",
"is_dxf_object",
]
logger = logging.getLogger("ezdxf")
class DXFObject(DXFEntity):
"""Non-graphical entities stored in the OBJECTS section."""
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
@register_entity
class Placeholder(DXFObject):
DXFTYPE = "ACDBPLACEHOLDER"
acdb_xrecord = DefSubclass(
"AcDbXrecord",
{
# 0 = not applicable
# 1 = keep existing
# 2 = use clone
# 3 = <xref>$0$<name>
# 4 = $0$<name>
# 5 = Unmangle name
"cloning": DXFAttr(
280,
default=1,
validator=validator.is_in_integer_range(0, 6),
fixer=RETURN_DEFAULT,
),
},
)
def totags(tags: Iterable) -> Iterable[DXFTag]:
for tag in tags:
if isinstance(tag, DXFTag):
yield tag
else:
yield dxftag(tag[0], tag[1])
@register_entity
class XRecord(DXFObject):
"""DXF XRECORD entity"""
DXFTYPE = "XRECORD"
DXFATTRIBS = DXFAttributes(base_class, acdb_xrecord)
def __init__(self):
super().__init__()
self.tags = Tags()
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, XRecord)
entity.tags = Tags(self.tags)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
try:
tags = processor.subclasses[1]
except IndexError:
raise DXFStructureError(
f"Missing subclass AcDbXrecord in XRecord (#{dxf.handle})"
)
start_index = 1
if len(tags) > 1:
# First tag is group code 280, but not for DXF R13/R14.
# SUT: doc may be None, but then doc also can not
# be R13/R14 - ezdxf does not create R13/R14
if self.doc is None or self.doc.dxfversion >= DXF2000:
code, value = tags[1]
if code == 280:
dxf.cloning = value
start_index = 2
else: # just log recoverable error
logger.info(
f"XRecord (#{dxf.handle}): expected group code 280 "
f"as first tag in AcDbXrecord"
)
self.tags = Tags(tags[start_index:])
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_xrecord.name)
tagwriter.write_tag2(280, self.dxf.cloning)
tagwriter.write_tags(Tags(totags(self.tags)))
def reset(self, tags: Iterable[Union[DXFTag, tuple[int, Any]]]) -> None:
"""Reset DXF tags."""
self.tags.clear()
self.tags.extend(totags(tags))
def extend(self, tags: Iterable[Union[DXFTag, tuple[int, Any]]]) -> None:
"""Extend DXF tags."""
self.tags.extend(totags(tags))
def clear(self) -> None:
"""Remove all DXF tags."""
self.tags.clear()
acdb_vba_project = DefSubclass(
"AcDbVbaProject",
{
# 90: Number of bytes of binary chunk data (contained in the group code
# 310 records that follow)
# 310: DXF: Binary object data (multiple entries containing VBA project
# data)
},
)
@register_entity
class VBAProject(DXFObject):
"""DXF VBA_PROJECT entity"""
DXFTYPE = "VBA_PROJECT"
DXFATTRIBS = DXFAttributes(base_class, acdb_vba_project)
def __init__(self):
super().__init__()
self.data = b""
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, VBAProject)
entity.data = entity.data
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
self.load_byte_data(processor.subclasses[1])
return dxf
def load_byte_data(self, tags: Tags) -> None:
byte_array = array.array("B")
# Translation from String to binary data happens in tag_compiler():
for byte_data in (tag.value for tag in tags if tag.code == 310):
byte_array.extend(byte_data)
self.data = byte_array.tobytes()
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_vba_project.name)
tagwriter.write_tag2(90, len(self.data))
self.export_data(tagwriter)
def export_data(self, tagwriter: AbstractTagWriter):
data = self.data
while data:
tagwriter.write_tag(DXFBinaryTag(310, data[:127]))
data = data[127:]
def clear(self) -> None:
self.data = b""
acdb_sort_ents_table = DefSubclass(
"AcDbSortentsTable",
{
# Soft-pointer ID/handle to owner (currently only the *MODEL_SPACE or
# *PAPER_SPACE blocks) in ezdxf the block_record handle for a layout is
# also called layout_key:
"block_record_handle": DXFAttr(330),
# 331: Soft-pointer ID/handle to an entity (zero or more entries may exist)
# 5: Sort handle (zero or more entries may exist)
},
)
acdb_sort_ents_table_group_codes = group_code_mapping(acdb_sort_ents_table)
@register_entity
class SortEntsTable(DXFObject):
"""DXF SORTENTSTABLE entity - sort entities table"""
# should work with AC1015/R2000 but causes problems with TrueView/AutoCAD
# LT 2019: "expected was-a-zombie-flag"
# No problems with AC1018/R2004 and later
#
# If the header variable $SORTENTS Regen flag (bit-code value 16) is set,
# AutoCAD regenerates entities in ascending handle order.
#
# When the DRAWORDER command is used, a SORTENTSTABLE object is attached to
# the *Model_Space or *Paper_Space block's extension dictionary under the
# name ACAD_SORTENTS. The SORTENTSTABLE object related to this dictionary
# associates a different handle with each entity, which redefines the order
# in which the entities are regenerated.
#
# $SORTENTS (280): Controls the object sorting methods (bitcode):
# 0 = Disables SORTENTS
# 1 = Sorts for object selection
# 2 = Sorts for object snap
# 4 = Sorts for redraws; obsolete
# 8 = Sorts for MSLIDE command slide creation; obsolete
# 16 = Sorts for REGEN commands
# 32 = Sorts for plotting
# 64 = Sorts for PostScript output; obsolete
DXFTYPE = "SORTENTSTABLE"
DXFATTRIBS = DXFAttributes(base_class, acdb_sort_ents_table)
def __init__(self) -> None:
super().__init__()
self.table: dict[str, str] = dict()
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, SortEntsTable)
entity.table = dict(entity.table)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
tags = processor.fast_load_dxfattribs(
dxf, acdb_sort_ents_table_group_codes, 1, log=False
)
self.load_table(tags)
return dxf
def load_table(self, tags: Tags) -> None:
for handle, sort_handle in take2(tags):
if handle.code != 331:
raise DXFStructureError(
f"Invalid handle code {handle.code}, expected 331"
)
if sort_handle.code != 5:
raise DXFStructureError(
f"Invalid sort handle code {handle.code}, expected 5"
)
self.table[handle.value] = sort_handle.value
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_sort_ents_table.name)
tagwriter.write_tag2(330, self.dxf.block_record_handle)
self.export_table(tagwriter)
def export_table(self, tagwriter: AbstractTagWriter):
for handle, sort_handle in self.table.items():
tagwriter.write_tag2(331, handle)
tagwriter.write_tag2(5, sort_handle)
def __len__(self) -> int:
return len(self.table)
def __iter__(self) -> Iterable:
"""Yields all redraw associations as (object_handle, sort_handle)
tuples.
"""
return iter(self.table.items())
def append(self, handle: str, sort_handle: str) -> None:
"""Append redraw association (handle, sort_handle).
Args:
handle: DXF entity handle (uppercase hex value without leading '0x')
sort_handle: sort handle (uppercase hex value without leading '0x')
"""
self.table[handle] = sort_handle
def clear(self):
"""Remove all handles from redraw order table."""
self.table = dict()
def set_handles(self, handles: Iterable[tuple[str, str]]) -> None:
"""Set all redraw associations from iterable `handles`, after removing
all existing associations.
Args:
handles: iterable yielding (object_handle, sort_handle) tuples
"""
# The sort_handle doesn't have to be unique, same or all handles can
# share the same sort_handle and sort_handles can use existing handles
# too.
#
# 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.
# Invalid entity handles will be ignored by AutoCAD.
self.table = dict(handles)
def remove_invalid_handles(self) -> None:
"""Remove all handles which do not exist in the drawing database."""
if self.doc is None:
return
entitydb = self.doc.entitydb
self.table = {
handle: sort_handle
for handle, sort_handle in self.table.items()
if handle in entitydb
}
def remove_handle(self, handle: str) -> None:
"""Remove handle of DXF entity from redraw order table.
Args:
handle: DXF entity handle (uppercase hex value without leading '0x')
"""
try:
del self.table[handle]
except KeyError:
pass
acdb_field = DefSubclass(
"AcDbField",
{
"evaluator_id": DXFAttr(1),
"field_code": DXFAttr(2),
# Overflow of field code string
"field_code_overflow": DXFAttr(3),
# Number of child fields
"n_child_fields": DXFAttr(90),
# 360: Child field ID (AcDbHardOwnershipId); repeats for number of children
# 97: Number of object IDs used in the field code
# 331: Object ID used in the field code (AcDbSoftPointerId); repeats for
# the number of object IDs used in the field code
# 93: Number of the data set in the field
# 6: Key string for the field data; a key-field pair is repeated for the
# number of data sets in the field
# 7: Key string for the evaluated cache; this key is hard-coded
# as ACFD_FIELD_VALUE
# 90: Data type of field value
# 91: Long value (if data type of field value is long)
# 140: Double value (if data type of field value is double)
# 330: ID value, AcDbSoftPointerId (if data type of field value is ID)
# 92: Binary data buffer size (if data type of field value is binary)
# 310: Binary data (if data type of field value is binary)
# 301: Format string
# 9: Overflow of Format string
# 98: Length of format string
},
)
# todo: implement FIELD
# register when done
class Field(DXFObject):
"""DXF FIELD entity"""
DXFTYPE = "FIELD"
DXFATTRIBS = DXFAttributes(base_class, acdb_field)
def is_dxf_object(entity: DXFEntity) -> TypeGuard[DXFObject]:
"""Returns ``True`` if the `entity` is a DXF object from the OBJECTS section,
otherwise the entity is a table or class entry or a graphic entity which can
not reside in the OBJECTS section.
"""
if isinstance(entity, DXFObject):
return True
if isinstance(entity, DXFTagStorage) and not entity.is_graphic_entity:
return True
return False