256 lines
7.9 KiB
Python
256 lines
7.9 KiB
Python
# Copyright (c) 2011-2023, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import (
|
|
Optional,
|
|
TYPE_CHECKING,
|
|
Iterable,
|
|
Iterator,
|
|
Callable,
|
|
Any,
|
|
Union,
|
|
NewType,
|
|
cast,
|
|
NamedTuple,
|
|
Mapping,
|
|
)
|
|
from enum import Enum
|
|
from .const import DXFAttributeError, DXF12
|
|
import copy
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.entities import DXFEntity
|
|
|
|
|
|
class DefSubclass(NamedTuple):
|
|
name: Optional[str]
|
|
attribs: dict[str, DXFAttr]
|
|
|
|
|
|
VIRTUAL_TAG = -666
|
|
|
|
|
|
class XType(Enum):
|
|
"""Extended Attribute Types"""
|
|
|
|
point2d = 1 # 2D points only
|
|
point3d = 2 # 3D points only
|
|
any_point = 3 # 2D or 3D points
|
|
callback = 4 # callback attribute
|
|
|
|
|
|
def group_code_mapping(
|
|
subclass: DefSubclass, *, ignore: Optional[Iterable[int]] = None
|
|
) -> dict[int, Union[str, list[str]]]:
|
|
# Unique group codes are stored as group_code <int>: name <str>
|
|
# Duplicate group codes are stored as group_code <int>: [name1, name2, ...] <list>
|
|
# The order of appearance is important, therefore also callback attributes
|
|
# have to be included, but they should not be loaded into the DXF namespace.
|
|
mapping: dict[int, Union[str, list[str]]] = dict()
|
|
for name, dxfattrib in subclass.attribs.items():
|
|
if dxfattrib.xtype == XType.callback:
|
|
# Mark callback attributes for special treatment as invalid
|
|
# Python name:
|
|
name = "*" + name
|
|
code = dxfattrib.code
|
|
existing_data: Union[None, str, list[str]] = mapping.get(code)
|
|
if existing_data is None:
|
|
mapping[code] = name
|
|
else:
|
|
if isinstance(existing_data, str):
|
|
existing_data = [existing_data]
|
|
mapping[code] = existing_data
|
|
assert isinstance(existing_data, list)
|
|
existing_data.append(name)
|
|
|
|
if ignore:
|
|
# Mark these tags as "*IGNORE" to be ignored,
|
|
# but they are not real callbacks! See POLYLINE for example!
|
|
for code in ignore:
|
|
mapping[code] = "*IGNORE"
|
|
return mapping
|
|
|
|
|
|
def merge_group_code_mappings(*mappings: Mapping) -> dict[int, str]:
|
|
merge_group_code_mapping: dict[int, str] = {}
|
|
for index, mapping in enumerate(mappings):
|
|
msg = f"{index}. mapping contains none unique group codes"
|
|
if not all(isinstance(e, str) for e in mapping.values()):
|
|
raise TypeError(msg)
|
|
if any(k in merge_group_code_mapping for k in mapping.keys()):
|
|
raise TypeError(msg)
|
|
merge_group_code_mapping.update(mapping)
|
|
return merge_group_code_mapping
|
|
|
|
|
|
# Unique object as marker
|
|
ReturnDefault = NewType("ReturnDefault", object)
|
|
RETURN_DEFAULT = cast(ReturnDefault, object())
|
|
|
|
|
|
class DXFAttr:
|
|
"""Represents a DXF attribute for an DXF entity, accessible by the
|
|
DXF namespace :attr:`DXFEntity.dxf` like ``entity.dxf.color = 7``.
|
|
This definitions are immutable by design not by implementation.
|
|
|
|
Extended Attribute Types
|
|
------------------------
|
|
|
|
- XType.point2d: 2D points only
|
|
- XType.point3d: 3D point only
|
|
- XType.any_point: mixed 2D/3D point
|
|
- XType.callback: Calls get_value(entity) to get the value of DXF
|
|
attribute 'name', and calls set_value(entity, value) to set value
|
|
of DXF attribute 'name'.
|
|
|
|
See example definition: ezdxf.entities.dxfgfx.acdb_entity.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
code: int,
|
|
xtype: Optional[XType] = None,
|
|
default=None,
|
|
optional: bool = False,
|
|
dxfversion: str = DXF12,
|
|
getter: str = "",
|
|
setter: str = "",
|
|
alias: str = "",
|
|
validator: Optional[Callable[[Any], bool]] = None,
|
|
fixer: Optional[Union[Callable[[Any], Any], None, ReturnDefault]] = None,
|
|
):
|
|
|
|
# Attribute name set by DXFAttributes.__init__()
|
|
self.name: str = ""
|
|
|
|
# DXF group code
|
|
self.code: int = code
|
|
|
|
# Extended attribute type:
|
|
self.xtype: Optional[XType] = xtype
|
|
|
|
# DXF default value
|
|
self.default: Any = default
|
|
|
|
# If optional is True, this attribute will be exported to DXF files
|
|
# only if the given value differs from default value.
|
|
self.optional: bool = optional
|
|
|
|
# This attribute is valid for all DXF versions starting from the
|
|
# specified DXF version, default is DXF12 = 'AC1009'
|
|
self.dxfversion: str = dxfversion
|
|
|
|
# DXF entity getter method name for callback attributes
|
|
self.getter: str = getter
|
|
|
|
# DXF entity setter method name for callback attributes
|
|
self.setter: str = setter
|
|
|
|
# Alternative name for this attribute
|
|
self.alias: str = alias
|
|
|
|
# Returns True if given value is valid - the validator should be as
|
|
# fast as possible!
|
|
self.validator: Optional[Callable[[Any], bool]] = validator
|
|
|
|
# Returns a fixed value for invalid attributes, the fixer is called
|
|
# only if the validator returns False.
|
|
if fixer is RETURN_DEFAULT:
|
|
fixer = self._return_default
|
|
# excluding ReturnDefault type
|
|
self.fixer = cast(Optional[Callable[[Any], Any]], fixer)
|
|
|
|
def _return_default(self, x: Any) -> Any:
|
|
return self.default
|
|
|
|
def __str__(self) -> str:
|
|
return f"({self.name}, {self.code})"
|
|
|
|
def __repr__(self) -> str:
|
|
return "DXFAttr" + self.__str__()
|
|
|
|
def get_callback_value(self, entity: DXFEntity) -> Any:
|
|
"""
|
|
Executes a callback function in 'entity' to get a DXF value.
|
|
|
|
Callback function is defined by self.getter as string.
|
|
|
|
Args:
|
|
entity: DXF entity
|
|
|
|
Raises:
|
|
AttributeError: getter method does not exist
|
|
TypeError: getter is None
|
|
|
|
Returns:
|
|
DXF attribute value
|
|
|
|
"""
|
|
try:
|
|
return getattr(entity, self.getter)()
|
|
except AttributeError:
|
|
raise DXFAttributeError(
|
|
f"DXF attribute {self.name}: invalid getter {self.getter}."
|
|
)
|
|
except TypeError:
|
|
raise DXFAttributeError(f"DXF attribute {self.name} has no getter.")
|
|
|
|
def set_callback_value(self, entity: DXFEntity, value: Any) -> None:
|
|
"""Executes a callback function in 'entity' to set a DXF value.
|
|
|
|
Callback function is defined by self.setter as string.
|
|
|
|
Args:
|
|
entity: DXF entity
|
|
value: DXF attribute value
|
|
|
|
Raises:
|
|
AttributeError: setter method does not exist
|
|
TypeError: setter is None
|
|
|
|
"""
|
|
try:
|
|
getattr(entity, self.setter)(value)
|
|
except AttributeError:
|
|
raise DXFAttributeError(
|
|
f"DXF attribute {self.name}: invalid setter {self.setter}."
|
|
)
|
|
except TypeError:
|
|
raise DXFAttributeError(f"DXF attribute {self.name} has no setter.")
|
|
|
|
def is_valid_value(self, value: Any) -> bool:
|
|
if self.validator:
|
|
return self.validator(value)
|
|
else:
|
|
return True
|
|
|
|
|
|
class DXFAttributes:
|
|
__slots__ = ("_attribs",)
|
|
|
|
def __init__(self, *subclassdefs: DefSubclass):
|
|
self._attribs: dict[str, DXFAttr] = dict()
|
|
for subclass in subclassdefs:
|
|
for name, dxfattrib in subclass.attribs.items():
|
|
dxfattrib.name = name
|
|
self._attribs[name] = dxfattrib
|
|
if dxfattrib.alias:
|
|
alias = copy.copy(dxfattrib)
|
|
alias.name = dxfattrib.alias
|
|
alias.alias = dxfattrib.name
|
|
self._attribs[dxfattrib.alias] = alias
|
|
|
|
def __contains__(self, name: str) -> bool:
|
|
return name in self._attribs
|
|
|
|
def get(self, key: str) -> Optional[DXFAttr]:
|
|
return self._attribs.get(key)
|
|
|
|
def build_group_code_items(self, func=lambda x: True) -> Iterator[tuple[int, str]]:
|
|
return (
|
|
(attrib.code, name)
|
|
for name, attrib in self._attribs.items()
|
|
if attrib.code > 0 and func(name)
|
|
)
|