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

711 lines
26 KiB
Python

# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing_extensions import Self
import copy
from ezdxf.lldxf import validator
from ezdxf.math import NULLVEC, Vec3, Z_AXIS, OCS, Matrix44
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf import const
from ezdxf.lldxf.types import EMBEDDED_OBJ_MARKER, EMBEDDED_OBJ_STR
from ezdxf.enums import MAP_MTEXT_ALIGN_TO_FLAGS, TextHAlign, TextVAlign
from ezdxf.tools import set_flag_state
from ezdxf.tools.text import (
load_mtext_content,
fast_plain_mtext,
plain_mtext,
)
from .dxfns import SubclassProcessor, DXFNamespace
from .dxfentity import base_class
from .dxfgfx import acdb_entity, elevation_to_z_axis
from .text import Text, acdb_text, acdb_text_group_codes
from .mtext import (
acdb_mtext_group_codes,
MText,
export_mtext_content,
acdb_mtext,
)
from .factory import register_entity
from .copy import default_copy
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.lldxf.tags import Tags
from ezdxf.entities import DXFEntity
from ezdxf import xref
__all__ = ["AttDef", "Attrib", "copy_attrib_as_text", "BaseAttrib"]
# Where is it valid to place an ATTRIB entity:
# - YES: attached to an INSERT entity
# - NO: stand-alone entity in model space - ignored by BricsCAD and TrueView
# - NO: stand-alone entity in paper space - ignored by BricsCAD and TrueView
# - NO: stand-alone entity in block layout - ignored by BricsCAD and TrueView
#
# The RECOVER command of BricsCAD removes the stand-alone ATTRIB entities:
# "Invalid subentity type AcDbAttribute(<handle>)"
#
# IMPORTANT: placing ATTRIB at an invalid layout does NOT create an invalid DXF file!
#
# Where is it valid to place an ATTDEF entity:
# - NO: attached to an INSERT entity
# - YES: stand-alone entity in a BLOCK layout - BricsCAD and TrueView render the
# TAG in the block editor and does not render the ATTDEF as block content
# for the INSERT entity.
# - YES: stand-alone entity in model space - BricsCAD and TrueView render the
# TAG not the default text - the model space is also a block content
# (XREF, see also INSERT entity)
# - YES: stand-alone entity in paper space - same as model space, although a
# paper space can not be used as XREF.
# DXF Reference for ATTRIB is a total mess and incorrect, the AcDbText subclass
# for the ATTRIB entity is the same as for the TEXT entity, but the valign field
# from the 2nd AcDbText subclass of the TEXT entity is stored in the
# AcDbAttribute subclass:
attrib_fields = {
# "version": DXFAttr(280, default=0, dxfversion=const.DXF2010),
# The "version" tag has the same group code as the lock_position tag!!!!!
# Version number: 0 = 2010
# This tag is not really used (at least by BricsCAD) but there exists DXF files
# which do use this tag: "dxftest\attrib\attrib_with_mtext_R2018.dxf"
# ezdxf stores the last group code 280 as "lock_position" attribute and does
# not export a version tag for any DXF version.
# Tag string (cannot contain spaces):
"tag": DXFAttr(
2,
default="",
validator=validator.is_valid_attrib_tag,
fixer=validator.fix_attrib_tag,
),
# 1 = Attribute is invisible (does not appear)
# 2 = This is a constant attribute
# 4 = Verification is required on input of this attribute
# 8 = Attribute is preset (no prompt during insertion)
"flags": DXFAttr(70, default=0),
# Field length (optional) (not currently used)
"field_length": DXFAttr(73, default=0, optional=True),
# Vertical text justification type (optional); see group code 73 in TEXT
"valign": DXFAttr(
74,
default=0,
optional=True,
validator=validator.is_in_integer_range(0, 4),
fixer=RETURN_DEFAULT,
),
# Lock position flag. Locks the position of the attribute within the block
# reference, example of double use of group codes in one sub class
"lock_position": DXFAttr(
280,
default=0,
dxfversion=const.DXF2007, # tested with BricsCAD 2023/TrueView 2023
optional=True,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# Attribute type:
# 1 = single line
# 2 = multiline ATTRIB
# 4 = multiline ATTDEF
"attribute_type": DXFAttr(
71,
default=1,
dxfversion=const.DXF2018,
optional=True,
validator=validator.is_one_of({1, 2, 4}),
fixer=RETURN_DEFAULT,
),
}
# ATTDEF has an additional field: 'prompt'
# DXF attribute definitions are immutable, a shallow copy is sufficient:
attdef_fields = dict(attrib_fields)
attdef_fields["prompt"] = DXFAttr(
3,
default="",
validator=validator.is_valid_one_line_text,
fixer=validator.fix_one_line_text,
)
acdb_attdef = DefSubclass("AcDbAttributeDefinition", attdef_fields)
acdb_attdef_group_codes = group_code_mapping(acdb_attdef)
acdb_attrib = DefSubclass("AcDbAttribute", attrib_fields)
acdb_attrib_group_codes = group_code_mapping(acdb_attrib)
# --------------------------------------------------------------------------------------
# Does subclass AcDbXrecord really exist? Only the documentation in the DXF reference
# exists, no real world examples seen so far - it wouldn't be the first error or misleading
# information in the DXF reference.
# --------------------------------------------------------------------------------------
# For XRECORD the tag order is important and group codes appear multiple times,
# therefore this attribute definition needs a special treatment!
acdb_attdef_xrecord = DefSubclass(
"AcDbXrecord",
[ # type: ignore
# Duplicate record cloning flag (determines how to merge duplicate entries):
# 1 = Keep existing
("cloning", DXFAttr(280, default=1)),
# MText flag:
# 2 = multiline attribute
# 4 = constant multiline attribute definition
("mtext_flag", DXFAttr(70, default=0)),
# isReallyLocked flag:
# 0 = unlocked
# 1 = locked
(
"really_locked",
DXFAttr(
70,
default=0,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
),
# Number of secondary attributes or attribute definitions:
("secondary_attribs_count", DXFAttr(70, default=0)),
# Hard-pointer id of secondary attribute(s) or attribute definition(s):
("secondary_attribs_handle", DXFAttr(340, default="0")),
# Alignment point of attribute or attribute definition:
("align_point", DXFAttr(10, xtype=XType.point3d, default=NULLVEC)),
("current_annotation_scale", DXFAttr(40, default=0)),
# attribute or attribute definition tag string
(
"tag",
DXFAttr(
2,
default="",
validator=validator.is_valid_attrib_tag,
fixer=validator.fix_attrib_tag,
),
),
],
)
# Just for documentation:
# The "attached" MTEXT feature most likely does not exist!
#
# A special MTEXT entity can follow the ATTDEF and ATTRIB entity, which starts
# as a usual DXF entity with (0, 'MTEXT'), so processing can't be done here,
# because for ezdxf is this a separated Entity.
#
# The attached MTEXT entity: owner is None and handle is None
# Linked as attribute `attached_mtext`.
# I don't have seen this combination of entities in real world examples and is
# ignored by ezdxf for now.
#
# No DXF files available which uses this feature - misleading DXF Reference!?
# Attrib and Attdef can have embedded MTEXT entities located in the
# <Embedded Object> subclass, see issue #258
class BaseAttrib(Text):
XRECORD_DEF = acdb_attdef_xrecord
def __init__(self) -> None:
super().__init__()
# Does subclass AcDbXrecord really exist?
self._xrecord: Optional[Tags] = None
self._embedded_mtext: Optional[EmbeddedMText] = None
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
"""Copy entity data, xrecord data and embedded MTEXT are not stored
in the entity database.
"""
assert isinstance(entity, BaseAttrib)
entity._xrecord = copy.deepcopy(self._xrecord)
entity._embedded_mtext = copy.deepcopy(self._embedded_mtext)
def load_embedded_mtext(self, processor: SubclassProcessor) -> None:
if not processor.embedded_objects:
return
embedded_object = processor.embedded_objects[0]
if embedded_object:
mtext = EmbeddedMText()
mtext.load_dxf_tags(processor)
self._embedded_mtext = mtext
def export_dxf_r2018_features(self, tagwriter: AbstractTagWriter) -> None:
tagwriter.write_tag2(71, self.dxf.attribute_type)
tagwriter.write_tag2(72, 0) # unknown tag
if self.dxf.hasattr("align_point"):
# duplicate align point - why?
tagwriter.write_vertex(11, self.dxf.align_point)
if self._xrecord:
tagwriter.write_tags(self._xrecord)
if self._embedded_mtext:
self._embedded_mtext.export_dxf_tags(tagwriter)
@property
def is_const(self) -> bool:
"""This is a constant attribute if ``True``."""
return bool(self.dxf.flags & const.ATTRIB_CONST)
@is_const.setter
def is_const(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_CONST, state)
@property
def is_invisible(self) -> bool:
"""Attribute is invisible if ``True``."""
return bool(self.dxf.flags & const.ATTRIB_INVISIBLE)
@is_invisible.setter
def is_invisible(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_INVISIBLE, state)
@property
def is_verify(self) -> bool:
"""Verification is required on input of this attribute. (interactive CAD
application feature)
"""
return bool(self.dxf.flags & const.ATTRIB_VERIFY)
@is_verify.setter
def is_verify(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_VERIFY, state)
@property
def is_preset(self) -> bool:
"""No prompt during insertion. (interactive CAD application feature)"""
return bool(self.dxf.flags & const.ATTRIB_IS_PRESET)
@is_preset.setter
def is_preset(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_IS_PRESET, state)
@property
def has_embedded_mtext_entity(self) -> bool:
"""Returns ``True`` if the entity has an embedded MTEXT entity for multi-line
support.
"""
return bool(self._embedded_mtext)
def virtual_mtext_entity(self) -> MText:
"""Returns the embedded MTEXT entity as a regular but virtual
:class:`MText` entity with the same graphical properties as the
host entity.
"""
if not self._embedded_mtext:
raise TypeError("no embedded MTEXT entity exist")
mtext = self._embedded_mtext.virtual_mtext_entity()
mtext.update_dxf_attribs(self.graphic_properties())
return mtext
def plain_mtext(self, fast=True) -> str:
"""Returns the embedded MTEXT content without formatting codes.
Returns an empty string if no embedded MTEXT entity exist.
The `fast` mode is accurate if the DXF content was created by
reliable (and newer) CAD applications like AutoCAD or BricsCAD.
The `accurate` mode is for some rare cases where the content was
created by older CAD applications or unreliable DXF libraries and CAD
applications.
The `accurate` mode is **much** slower than the `fast` mode.
Args:
fast: uses the `fast` mode to extract the plain MTEXT content if
``True`` or the `accurate` mode if set to ``False``
"""
if self._embedded_mtext:
text = self._embedded_mtext.text
if fast:
return fast_plain_mtext(text, split=False) # type: ignore
else:
return plain_mtext(text, split=False) # type: ignore
return ""
def set_mtext(self, mtext: MText, graphic_properties=True) -> None:
"""Set multi-line properties from a :class:`MText` entity.
The multi-line ATTRIB/ATTDEF entity requires DXF R2018, otherwise an
ordinary single line ATTRIB/ATTDEF entity will be exported.
Args:
mtext: source :class:`MText` entity
graphic_properties: copy graphic properties (color, layer, ...) from
source MTEXT if ``True``
"""
if self._embedded_mtext is None:
self._embedded_mtext = EmbeddedMText()
self._embedded_mtext.set_mtext(mtext)
_update_content_from_mtext(self, mtext)
_update_location_from_mtext(self, mtext)
# misc properties
self.dxf.style = mtext.dxf.style
self.dxf.height = mtext.dxf.char_height
self.dxf.discard("width") # controlled in MTEXT by inline codes!
self.dxf.discard("oblique") # controlled in MTEXT by inline codes!
self.dxf.discard("text_generation_flag")
if graphic_properties:
self.update_dxf_attribs(mtext.graphic_properties())
def embed_mtext(self, mtext: MText, graphic_properties=True) -> None:
"""Set multi-line properties from a :class:`MText` entity and destroy the
source entity afterwards.
The multi-line ATTRIB/ATTDEF entity requires DXF R2018, otherwise an
ordinary single line ATTRIB/ATTDEF entity will be exported.
Args:
mtext: source :class:`MText` entity
graphic_properties: copy graphic properties (color, layer, ...) from
source MTEXT if ``True``
"""
self.set_mtext(mtext, graphic_properties)
mtext.destroy()
def register_resources(self, registry: xref.Registry) -> None:
"""Register required resources to the resource registry."""
super().register_resources(registry)
if self._embedded_mtext:
self._embedded_mtext.register_resources(registry)
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
assert isinstance(clone, BaseAttrib)
super().map_resources(clone, mapping)
if self._embedded_mtext and clone._embedded_mtext:
self._embedded_mtext.map_resources(clone._embedded_mtext, mapping)
# todo: map handles in embedded XRECORD if a real world example shows up
def transform(self, m: Matrix44) -> Self:
if self._embedded_mtext is None:
super().transform(m)
else:
mtext = self._embedded_mtext.virtual_mtext_entity()
mtext.transform(m)
self.set_mtext(mtext, graphic_properties=False)
self.post_transform(m)
return self
def _update_content_from_mtext(text: Text, mtext: MText) -> None:
content = mtext.plain_text(split=True, fast=True)
if content:
# In contrast to AutoCAD, just set the first line as single line
# ATTRIB content. AutoCAD concatenates all lines into a single
# "Line1\PLine2\P...", which (imho) is not very useful.
text.dxf.text = content[0]
def _update_location_from_mtext(text: Text, mtext: MText) -> None:
# TEXT is an OCS entity, MTEXT is a WCS entity
dxf = text.dxf
insert = Vec3(mtext.dxf.insert)
extrusion = Vec3(mtext.dxf.extrusion)
text_direction = mtext.get_text_direction()
if extrusion.isclose(Z_AXIS): # most common case
dxf.rotation = text_direction.angle_deg
else:
ocs = OCS(extrusion)
insert = ocs.from_wcs(insert)
dxf.extrusion = extrusion.normalize()
dxf.rotation = ocs.from_wcs(text_direction).angle_deg
dxf.insert = insert
dxf.align_point = insert # the same point for all MTEXT alignments!
dxf.halign, dxf.valign = MAP_MTEXT_ALIGN_TO_FLAGS.get(
mtext.dxf.attachment_point, (TextHAlign.LEFT, TextVAlign.TOP)
)
@register_entity
class AttDef(BaseAttrib):
"""DXF ATTDEF entity"""
DXFTYPE = "ATTDEF"
# Don't add acdb_attdef_xrecord here:
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_text, acdb_attdef)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super(Text, self).load_dxf_attribs(processor)
# Do not call Text loader.
if processor:
processor.fast_load_dxfattribs(dxf, acdb_text_group_codes, 2, recover=True)
processor.fast_load_dxfattribs(
dxf, acdb_attdef_group_codes, 3, recover=True
)
self._xrecord = processor.find_subclass(self.XRECORD_DEF.name) # type: ignore
self.load_embedded_mtext(processor)
if processor.r12:
# Transform elevation attribute from R11 to z-axis values:
elevation_to_z_axis(dxf, ("insert", "align_point"))
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
# Text() writes 2x AcDbText which is not suitable for AttDef()
self.export_acdb_entity(tagwriter)
self.export_acdb_text(tagwriter)
self.export_acdb_attdef(tagwriter)
if tagwriter.dxfversion >= const.DXF2018:
self.dxf.attribute_type = 4 if self.has_embedded_mtext_entity else 1
self.export_dxf_r2018_features(tagwriter)
def export_acdb_attdef(self, tagwriter: AbstractTagWriter) -> None:
if tagwriter.dxfversion > const.DXF12:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_attdef.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
# write version tag (280, 0) here, if required in the future
"prompt",
"tag",
"flags",
"field_length",
"valign",
"lock_position",
],
)
@register_entity
class Attrib(BaseAttrib):
"""DXF ATTRIB entity"""
DXFTYPE = "ATTRIB"
# Don't add acdb_attdef_xrecord here:
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_text, acdb_attrib)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super(Text, self).load_dxf_attribs(processor)
# Do not call Text loader.
if processor:
processor.fast_load_dxfattribs(dxf, acdb_text_group_codes, 2, recover=True)
processor.fast_load_dxfattribs(
dxf, acdb_attrib_group_codes, 3, recover=True
)
self._xrecord = processor.find_subclass(self.XRECORD_DEF.name) # type: ignore
self.load_embedded_mtext(processor)
if processor.r12:
# Transform elevation attribute from R11 to z-axis values:
elevation_to_z_axis(dxf, ("insert", "align_point"))
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
# Text() writes 2x AcDbText which is not suitable for AttDef()
self.export_acdb_entity(tagwriter)
self.export_acdb_attrib_text(tagwriter)
self.export_acdb_attrib(tagwriter)
if tagwriter.dxfversion >= const.DXF2018:
self.dxf.attribute_type = 2 if self.has_embedded_mtext_entity else 1
self.export_dxf_r2018_features(tagwriter)
def export_acdb_attrib_text(self, tagwriter: AbstractTagWriter) -> None:
# Despite the similarities to TEXT, it is different to
# Text.export_acdb_text():
if tagwriter.dxfversion > const.DXF12:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_text.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
"insert",
"height",
"text",
"thickness",
"rotation",
"oblique",
"style",
"width",
"halign",
"align_point",
"text_generation_flag",
"extrusion",
],
)
def export_acdb_attrib(self, tagwriter: AbstractTagWriter) -> None:
if tagwriter.dxfversion > const.DXF12:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_attrib.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
# write version tag (280, 0) here, if required in the future
"tag",
"flags",
"field_length",
"valign",
"lock_position",
],
)
IGNORE_FROM_ATTRIB = {
"handle",
"owner",
"version",
"prompt",
"tag",
"flags",
"field_length",
"lock_position",
}
def copy_attrib_as_text(attrib: BaseAttrib):
"""Returns the content of the ATTRIB/ATTDEF entity as a new virtual TEXT or
MTEXT entity.
"""
if attrib.has_embedded_mtext_entity:
return attrib.virtual_mtext_entity()
dxfattribs = attrib.dxfattribs(drop=IGNORE_FROM_ATTRIB)
return Text.new(dxfattribs=dxfattribs, doc=attrib.doc)
class EmbeddedMTextNS(DXFNamespace):
_DXFATTRIBS = DXFAttributes(acdb_mtext)
@property
def dxfattribs(self) -> DXFAttributes:
return self._DXFATTRIBS
@property
def dxftype(self) -> str:
return "Embedded MText"
class EmbeddedMText:
"""Representation of the embedded MTEXT object in ATTRIB and ATTDEF.
Introduced in DXF R2018? The DXF reference of the `MTEXT`_ entity
documents only the attached MTEXT entity. The ODA DWG specs includes all
MTEXT attributes of MTEXT starting at group code 10
Stores the required parameters to be shown as as MTEXT.
The AcDbText subclass contains the first line of the embedded MTEXT as
plain text content as group code 1, but this tag seems not to be maintained
if the ATTRIB entity is copied.
Some DXF attributes are duplicated and maintained by the CAD application:
- textstyle: same group code 7 (AcDbText, EmbeddedObject)
- text (char) height: same group code 40 (AcDbText, EmbeddedObject)
.. _MTEXT: https://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-7DD8B495-C3F8-48CD-A766-14F9D7D0DD9B
"""
def __init__(self) -> None:
# Attribute "dxf" contains the DXF attributes defined in subclass
# "AcDbMText"
self.dxf = EmbeddedMTextNS()
self.text: str = ""
def copy(self) -> EmbeddedMText:
copy_ = EmbeddedMText()
copy_.dxf = copy.deepcopy(self.dxf)
return copy_
__copy__ = copy
def load_dxf_tags(self, processor: SubclassProcessor) -> None:
tags = processor.fast_load_dxfattribs(
self.dxf,
group_code_mapping=acdb_mtext_group_codes,
subclass=processor.embedded_objects[0],
recover=False,
)
self.text = load_mtext_content(tags)
def virtual_mtext_entity(self) -> MText:
"""Returns the embedded MTEXT entity as regular but virtual MTEXT
entity. This entity does not have the graphical attributes of the host
entity (ATTRIB/ATTDEF).
"""
mtext = MText.new(dxfattribs=self.dxf.all_existing_dxf_attribs())
mtext.text = self.text
return mtext
def set_mtext(self, mtext: MText) -> None:
"""Set embedded MTEXT attributes from given `mtext` entity."""
self.text = mtext.text
dxf = self.dxf
for k, v in mtext.dxf.all_existing_dxf_attribs().items():
if dxf.is_supported(k):
dxf.set(k, v)
def set_required_dxf_attributes(self):
# These attributes are always present in DXF files created by Autocad:
dxf = self.dxf
for key, default in (
("insert", NULLVEC),
("char_height", 2.5),
("width", 0.0),
("defined_height", 0.0),
("attachment_point", 1),
("flow_direction", 5),
("style", "Standard"),
("line_spacing_style", 1),
("line_spacing_factor", 1.0),
):
if not dxf.hasattr(key):
dxf.set(key, default)
def export_dxf_tags(self, tagwriter: AbstractTagWriter) -> None:
"""Export embedded MTEXT as "Embedded Object"."""
tagwriter.write_tag2(EMBEDDED_OBJ_MARKER, EMBEDDED_OBJ_STR)
self.set_required_dxf_attributes()
self.dxf.export_dxf_attribs(
tagwriter,
[
"insert",
"char_height",
"width",
"defined_height",
"attachment_point",
"flow_direction",
],
)
export_mtext_content(self.text, tagwriter)
self.dxf.export_dxf_attribs(
tagwriter,
[
"style",
"extrusion",
"text_direction",
"rect_width",
"rect_height",
"rotation",
"line_spacing_style",
"line_spacing_factor",
"box_fill_scale",
"bg_fill",
"bg_fill_color",
"bg_fill_true_color",
"bg_fill_color_name",
"bg_fill_transparency",
],
)
def register_resources(self, registry: xref.Registry) -> None:
"""Register required resources to the resource registry."""
if self.dxf.hasattr("style"):
registry.add_text_style(self.dxf.style)
def map_resources(self, clone: EmbeddedMText, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
if clone.dxf.hasattr("style"):
clone.dxf.style = mapping.get_text_style(clone.dxf.style)