fix(blocks): block_explode keeps attribute text as TEXT (BURST rule) on both engines

virtual_entities() yields geometry only, so an exploded tagged symbol lost
its tag value silently. Every attached ATTRIB now becomes a TEXT at the
same WCS placement/height/rotation/style/layer (invisible stays
invisible), reported as attribute_texts. COM reads GetAttributes before
Explode, deletes the ATTDEF placeholders Explode returns, AddText per
value, and deletes the reference ActiveX Explode leaves behind.

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-16 21:26:27 +03:00
co-authored by Claude Opus 5
parent b9510bf377
commit 19610b8f05
5 changed files with 247 additions and 6 deletions
+68 -2
View File
@@ -3299,11 +3299,77 @@ class ComBackend(AutoCADBackend):
return await self._run(_sync)
async def block_explode(self, handle) -> dict:
"""Explode an INSERT through ActiveX; ATTRIB values survive as TEXT.
AutoCAD's EXPLODE keeps the attribute *definitions* (the tag names as
ATTDEF entities) and discards the values; ActiveX ``Explode()`` does
the same and, unlike the command, leaves the original reference in
place. This is the BURST rule instead: each ATTRIB's value, placement,
height, rotation and layer are read before the explode, the ATTDEFs
the explode returns are deleted, one ``AddText`` per ATTRIB carries the
value (an invisible ATTRIB becomes an invisible TEXT), and the
reference itself is deleted. Unit-tested against a fake ActiveX
surface; ``Explode()``'s return shape is the ActiveX documented one
(an array of the new objects) and is exercised live by the settings
smoke.
"""
def _sync():
doc = _acad_doc()
ent = doc.HandleToObject(handle)
ent.Explode()
return {"ok": True, "exploded_handle": handle}
if ent.ObjectName != "AcDbBlockReference":
raise RuntimeError(f"Entity {handle} is not a block reference (INSERT)")
captured = []
try:
attrs = ent.GetAttributes()
except Exception as exc:
log.debug("GetAttributes failed before explode: %s", exc)
attrs = ()
for attr in attrs:
try:
invisible = bool(attr.Invisible)
except Exception:
invisible = False
captured.append(
{
"text": str(attr.TextString),
"insertion": tuple(attr.InsertionPoint),
"height": float(attr.Height),
"rotation": float(attr.Rotation),
"layer": str(attr.Layer),
"invisible": invisible,
}
)
exploded = ent.Explode()
inserted = []
for obj in exploded or ():
if obj.ObjectName == "AcDbAttributeDefinition":
obj.Delete() # the tag placeholder EXPLODE leaves; the value is below
continue
inserted.append(str(obj.Handle))
mspace = _msp()
attribute_texts = []
for item in captured:
ins = item["insertion"]
text = mspace.AddText(
item["text"],
_apoint(ins[0], ins[1], ins[2] if len(ins) > 2 else 0.0),
item["height"],
)
text.Rotation = item["rotation"]
text.Layer = item["layer"]
if item["invisible"]:
text.Visible = False
attribute_texts.append(str(text.Handle))
ent.Delete()
_regen()
return {
"ok": True,
"exploded_handle": handle,
"inserted_handles": inserted,
"attribute_texts": attribute_texts,
"backend": "com",
}
return await self._run(_sync)
+46 -2
View File
@@ -4521,21 +4521,65 @@ class EzdxfBackend(AutoCADBackend):
return await self._async(_sync)
#: ATTRIB DXF attributes carried onto the TEXT that replaces it on explode.
#: ``insert``/``align_point`` are already WCS on an attached ATTRIB.
_ATTRIB_TO_TEXT = (
"layer",
"color",
"style",
"height",
"rotation",
"width",
"oblique",
"halign",
"valign",
"text_generation_flag",
"insert",
"align_point",
)
async def block_explode(self, handle) -> dict:
"""Explode an INSERT into its members; ATTRIB values survive as TEXT.
``virtual_entities()`` yields the definition's geometry only — no
ATTDEF, no ATTRIB — so an exploded tagged symbol used to lose its tag
text silently. This is the BURST rule (Express Tools): every attached
ATTRIB becomes a TEXT at the same WCS placement, height, rotation,
style and layer, carrying the *value*; an invisible ATTRIB becomes an
invisible TEXT rather than appearing. The new handles are reported as
``attribute_texts``, separate from the geometry's ``inserted_handles``.
"""
def _sync():
ent = self._get_entity(handle)
if ent.dxftype() != "INSERT":
raise RuntimeError(f"Entity {handle} is not a block reference (INSERT)")
msp = self._msp()
# Decompose: add individual entities to modelspace
inserted = []
for sub in ent.virtual_entities():
sub_copy = sub.copy()
msp.add_entity(sub_copy)
inserted.append(sub_copy.dxf.handle)
attribute_texts = []
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)
if attrib.is_invisible:
text.dxf.invisible = 1
attribute_texts.append(text.dxf.handle)
msp.delete_entity(ent)
self._mark_dirty()
return {"ok": True, "inserted_handles": inserted}
return {
"ok": True,
"exploded_handle": handle,
"inserted_handles": inserted,
"attribute_texts": attribute_texts,
"backend": "ezdxf",
}
return await self._async(_sync)