fix(task-2): block_explode keeps the ATTRIB frame and writes into the owner layout
Two silent wrong-place defects in the BURST path. An ATTRIB is an OCS entity, so the TEXT that replaces it now carries extrusion/thickness with insert/align_point; a mirrored reference (ATTRIB reflected to (0,0,-1), INSERT +Z with yscale=-1) put the tag 50 units across the mirror axis with a clean audit. Members and TEXTs now go into the INSERT's owner layout (ezdxf get_layout, COM ObjectIdToObject(OwnerID)) instead of the current space: a sheet title block exploded while Model was current duplicated geometry into model space and then died on delete_entity headless, and on COM landed the tag text in model space with ok: True. A reference nested in a block definition is refused before any write on both engines. Five regression tests (mirrored INSERT, sheet-owned INSERT with Model current, nested INSERT, and the COM owner-block / nested-refusal call shapes) fail on the previous commit and pass here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
19610b8f05
commit
13851a3d20
+42
-2
@@ -193,6 +193,38 @@ def _msp():
|
||||
return doc.ModelSpace
|
||||
|
||||
|
||||
def _owner_layout_block(doc, ent, handle):
|
||||
"""The block table record that owns ``ent``, refused unless it is a layout.
|
||||
|
||||
``Explode()`` places the members in the owner space, so anything this
|
||||
method adds alongside them must go there too — never ``ActiveLayout``,
|
||||
which is whatever tab the user happens to have open. An owner that is a
|
||||
block definition means ``ent`` is a nested reference; exploding it in place
|
||||
would redefine the block under every other reference, so it is refused
|
||||
before any call is dispatched. An owner that cannot be read at all is
|
||||
refused too rather than guessed.
|
||||
"""
|
||||
try:
|
||||
owner = doc.ObjectIdToObject(ent.OwnerID)
|
||||
is_layout = bool(owner.IsLayout)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
f"Entity {handle} has no resolvable owner layout ({exc}); refusing to "
|
||||
"explode into a guessed space"
|
||||
) from exc
|
||||
if not is_layout:
|
||||
try:
|
||||
name = str(owner.Name)
|
||||
except Exception:
|
||||
name = "?"
|
||||
raise RuntimeError(
|
||||
f"Entity {handle} is nested inside block definition {name!r}; explode the "
|
||||
"outer reference instead (exploding here would redefine the block under "
|
||||
"every other reference)"
|
||||
)
|
||||
return owner
|
||||
|
||||
|
||||
def _require_block_defined(doc, name):
|
||||
"""``doc.Blocks.Item(name)``, or a ``ValueError`` before ``InsertBlock`` runs.
|
||||
|
||||
@@ -3312,6 +3344,14 @@ class ComBackend(AutoCADBackend):
|
||||
surface; ``Explode()``'s return shape is the ActiveX documented one
|
||||
(an array of the new objects) and is exercised live by the settings
|
||||
smoke.
|
||||
|
||||
The TEXTs go into the reference's *owner* block
|
||||
(``ObjectIdToObject(OwnerID)``), which is where ``Explode()`` puts the
|
||||
members; ``HandleToObject`` resolves a handle in any layout, so a title
|
||||
block on a sheet exploded while Model was active used to get its tag
|
||||
text in model space with ``ok: True``. A reference nested inside a
|
||||
block definition (owner ``IsLayout`` false) is refused before the
|
||||
explode is dispatched.
|
||||
"""
|
||||
|
||||
def _sync():
|
||||
@@ -3319,6 +3359,7 @@ class ComBackend(AutoCADBackend):
|
||||
ent = doc.HandleToObject(handle)
|
||||
if ent.ObjectName != "AcDbBlockReference":
|
||||
raise RuntimeError(f"Entity {handle} is not a block reference (INSERT)")
|
||||
owner = _owner_layout_block(doc, ent, handle)
|
||||
captured = []
|
||||
try:
|
||||
attrs = ent.GetAttributes()
|
||||
@@ -3347,11 +3388,10 @@ class ComBackend(AutoCADBackend):
|
||||
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(
|
||||
text = owner.AddText(
|
||||
item["text"],
|
||||
_apoint(ins[0], ins[1], ins[2] if len(ins) > 2 else 0.0),
|
||||
item["height"],
|
||||
|
||||
@@ -4521,8 +4521,38 @@ class EzdxfBackend(AutoCADBackend):
|
||||
|
||||
return await self._async(_sync)
|
||||
|
||||
@staticmethod
|
||||
def _owner_layout_for_explode(ent, handle: str):
|
||||
"""The layout that owns ``ent`` — model space or a paper-space layout.
|
||||
|
||||
Raises before any write when the owner is a block definition (a nested
|
||||
reference) or cannot be resolved at all; guessing the current space
|
||||
instead is how geometry ends up duplicated into the wrong tab.
|
||||
"""
|
||||
try:
|
||||
layout = ent.get_layout()
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
f"Entity {handle} has no resolvable owner layout ({exc}); refusing to "
|
||||
"explode into a guessed space"
|
||||
) from exc
|
||||
if layout is None or not layout.is_any_layout:
|
||||
owner = getattr(layout, "name", None) or ent.dxf.get("owner", "?")
|
||||
raise RuntimeError(
|
||||
f"Entity {handle} is nested inside block definition {owner!r}; explode "
|
||||
"the outer reference instead (exploding here would redefine the block "
|
||||
"under every other reference)"
|
||||
)
|
||||
return layout
|
||||
|
||||
#: ATTRIB DXF attributes carried onto the TEXT that replaces it on explode.
|
||||
#: ``insert``/``align_point`` are already WCS on an attached ATTRIB.
|
||||
#: An ATTRIB is an OCS entity: ``insert``/``align_point`` are numbers in the
|
||||
#: plane of its own ``extrusion``, so the frame travels with them. Without
|
||||
#: it the TEXT took the OCS numbers with the default +Z frame, and a
|
||||
#: mirrored reference (ezdxf reflects the ATTRIB to extrusion ``(0, 0, -1)``
|
||||
#: while the INSERT keeps +Z with ``yscale=-1``) put the tag 50 units away
|
||||
#: on the other side of the mirror axis — audit clean, ``plane_normal``
|
||||
#: None, nothing said. Same numbers in the same frame is the same place.
|
||||
_ATTRIB_TO_TEXT = (
|
||||
"layer",
|
||||
"color",
|
||||
@@ -4536,6 +4566,8 @@ class EzdxfBackend(AutoCADBackend):
|
||||
"text_generation_flag",
|
||||
"insert",
|
||||
"align_point",
|
||||
"extrusion",
|
||||
"thickness",
|
||||
)
|
||||
|
||||
async def block_explode(self, handle) -> dict:
|
||||
@@ -4548,13 +4580,21 @@ class EzdxfBackend(AutoCADBackend):
|
||||
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``.
|
||||
|
||||
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
|
||||
geometry copied into model space and then die on ``delete_entity``
|
||||
with the reference still alive on the sheet. A reference nested inside
|
||||
a block definition is refused before anything is written — exploding
|
||||
it there would redefine the block under every other reference.
|
||||
"""
|
||||
|
||||
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()
|
||||
msp = self._owner_layout_for_explode(ent, handle)
|
||||
inserted = []
|
||||
for sub in ent.virtual_entities():
|
||||
sub_copy = sub.copy()
|
||||
|
||||
Reference in New Issue
Block a user