fix(task-2): block_explode writes Normal as a VARIANT and keeps constant attributes as TEXT

COM: ``text.Normal`` was a plain Python tuple, which ActiveX marshals as
VT_ARRAY|VT_VARIANT and refuses with E_INVALIDARG (verified live on
AutoCAD 2026); because the write came after Explode(), the ATTDEF deletes
and AddText, the tool raised and left the drawing torn. It now goes
through ``_apoint`` like every other point write, and the fake ActiveX
surface refuses a bare tuple on Normal/InsertionPoint/TextAlignmentPoint
so a test cannot pin the wrong call shape again.

COM: every AcDbAttributeDefinition Explode() returned was deleted, but
GetAttributes() excludes constant attributes, so a constant attribute's
only visible text was destroyed with ok: True. A constant ATTDEF from the
explode (Constant member, or its tag in GetConstantAttributes()) is now
captured like an ATTRIB and replaced by a TEXT; placeholders are still
just deleted.

ezdxf: an AutoCAD-authored reference carries no ATTRIB for a constant
attribute and virtual_entities() skips ATTDEF, so the same text vanished
headlessly. A constant ATTDEF whose tag has no attached ATTRIB becomes a
TEXT transformed by the reference matrix, the path add_auto_attribs takes,
and is not converted twice when block_insert did attach one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-17 09:52:58 +03:00
co-authored by Claude Opus 5
parent 415de73137
commit b12fa854c1
4 changed files with 305 additions and 51 deletions
+30 -6
View File
@@ -4570,6 +4570,11 @@ class EzdxfBackend(AutoCADBackend):
"thickness",
)
@classmethod
def _text_attribs_of(cls, attrib) -> dict:
"""The ``_ATTRIB_TO_TEXT`` members ``attrib`` (ATTRIB or ATTDEF) carries."""
return {key: attrib.dxf.get(key) for key in cls._ATTRIB_TO_TEXT if attrib.dxf.hasattr(key)}
async def block_explode(self, handle) -> dict:
"""Explode an INSERT into its members; ATTRIB values survive as TEXT.
@@ -4581,6 +4586,16 @@ class EzdxfBackend(AutoCADBackend):
invisible TEXT rather than appearing. The new handles are reported as
``attribute_texts``, separate from the geometry's ``inserted_handles``.
A *constant* attribute (ATTDEF flag 2) carries its value in the
definition and, in an AutoCAD-authored file, has no ATTRIB on the
reference — ``virtual_entities()`` skips ATTDEF, so its visible text
used to vanish with a clean audit. BURST converts it to TEXT, so every
constant ATTDEF whose tag has no attached ATTRIB is copied as a TEXT
and transformed by the reference's matrix — the same path
``add_auto_attribs`` takes, so it lands where the ATTRIB the headless
``block_insert`` attaches would have (that ATTRIB, when present, is
converted above and the definition is not converted twice).
The members and the TEXTs go into the INSERT's *owner* layout, not the
current one: ``_get_entity`` resolves a handle in any layout, so a title
block on a sheet exploded while Model was current used to have its
@@ -4601,16 +4616,25 @@ class EzdxfBackend(AutoCADBackend):
msp.add_entity(sub_copy)
inserted.append(sub_copy.dxf.handle)
attribute_texts = []
attached_tags = set()
for attrib in ent.attribs:
dxfattribs = {
key: attrib.dxf.get(key)
for key in self._ATTRIB_TO_TEXT
if attrib.dxf.hasattr(key)
}
text = msp.add_text(attrib.dxf.text, dxfattribs=dxfattribs)
attached_tags.add(attrib.dxf.tag)
text = msp.add_text(attrib.dxf.text, dxfattribs=self._text_attribs_of(attrib))
if attrib.is_invisible:
text.dxf.invisible = 1
attribute_texts.append(text.dxf.handle)
block = ent.block()
attdefs = block.attdefs() if block is not None else ()
for attdef in attdefs:
if not attdef.is_const or attdef.dxf.get("tag") in attached_tags:
continue
if not attdef.dxf.hasattr("insert"):
continue # a structure error; nowhere to place it
text = msp.add_text(attdef.dxf.text, dxfattribs=self._text_attribs_of(attdef))
text.transform(ent.matrix44())
if attdef.is_invisible:
text.dxf.invisible = 1
attribute_texts.append(text.dxf.handle)
msp.delete_entity(ent)
self._mark_dirty()
return {