Two review findings on the headless engine, both content lost with ok: True.
Multi-line attribute writer: `block_set_attributes` set only `attrib.dxf.text`,
so after an edit `block_get_attributes` reported the new value while
`block_explode` (`_burst_attrib`) burst the embedded MTEXT at its pre-edit
content -- and on the default R2010 document, where ezdxf exports no embedded
MTEXT, a save/reload carried the new value the exploded drawing never showed.
`_write_attrib_value` now rebuilds the embedded MTEXT from its own virtual
entity with the new content (`virtual_mtext_entity()` + `set_mtext(...,
graphic_properties=False)`, the round trip ezdxf's `BaseAttrib.transform`
uses), keeping placement, style and height; `_attrib_value` reports the
embedded MTEXT's full `Line1\PLine2` content for a multi-line attribute, as
AutoCAD's `TextString` does, so get / set / explode / COM agree.
Opaque members: `block_explode` iterated `virtual_entities()` without a
`skipped_entity_callback`, so ezdxf dropped every member it cannot copy
(OLE2FRAME) or transform (VIEWPORT) with a debug log, an ACAD_PROXY_ENTITY
without proxy graphics contributed nothing, and the reference was then deleted.
`_explodable_members` materializes the list before any write and refuses by
member with the typed capability `explode_opaque_members` (declared on both
engines: headless False with the reason, COM native), naming the block and
the live way out. The block_explode docstrings (backend and tool) now state
this refusal instead of claiming the xref/MINSERT gate covered everything.
Tests: eight new headless cases in tests/test_track_a_hardening.py -- the
multi-line reader, the rewrite (value on every surface, placement unchanged,
clean audit), a `\P` value and the empty value, an R2010 save/reload, the
single-line path unchanged, the opaque-member refusal (nothing written,
reference alive, members named), an ordinary block still exploding under a
uniform scale, and the capability declared on both engines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
A multi-line ATTRIB keeps its content in an embedded MTEXT; dxf.text is only
the first line (ezdxf) or "Line1\PLine2" (AutoCAD), so the TEXT the explode
emitted silently dropped lines or rendered literal \P with a clean audit.
ezdxf: `_burst_attrib` adds `virtual_mtext_entity()` (graphic properties and
WCS placement already carried; the reference matrix applied for a constant
ATTDEF) and marks it invisible like the ATTRIB. COM: `_capture_attrib` reads
`MTextAttribute` / `MTextAttributeContent` / `MTextBoundaryWidth` and
`_add_mtext_from_capture` goes through `AddMText` (height, style, frame,
then Rotation, then the attachment mapped from the ATTRIB's Alignment).
Two INSERT kinds `virtual_entities()` cannot explode were destroyed with
ok: True: an xref (no virtual entities, reference deleted) and a MINSERT
(only the first cell resolved, the rest of the grid gone). Both are now
refused by name before any write, on both engines (`block_record.is_xref`
/ `mcount > 1` headless; `Blocks.Item(Name).IsXRef` / `AcDbMInsertBlock`
live, the latter previously failing the INSERT check with the misleading
"not a block reference").
Tests: nine new cases in tests/test_track_a_hardening.py (headless multi-line
ATTRIB and constant ATTDEF, xref and MINSERT refusals, a single-cell grid
that still explodes; COM multi-line capture, frame-before-angle, and both
named refusals). The fake ActiveX surface gains AddMText and IsXRef.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
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
The BURST path on the live engine read TextString/InsertionPoint/Height/
Rotation/Layer/Invisible and nothing else, so the +Z TEXT that AddText
builds got the ATTRIB's OCS Rotation with no Normal: a mirrored
reference's tag (Normal (0,0,-1), the same premise the ezdxf regression
pins) came back reading theta instead of 180deg-theta, un-mirrored, on
the wrong side of its anchor at theta=0, with ok: True. Style members
(StyleName, ObliqueAngle, ScaleFactor, Backward/UpsideDown, Alignment +
TextAlignmentPoint, Color) were dropped the same way.
_capture_attrib_text_members reads the ActiveX twins of the headless
_ATTRIB_TO_TEXT carry, guarded per member and never written as a guess;
a +Z Normal is not carried and a left-aligned ATTRIB drops its
TextAlignmentPoint (ActiveX refuses it). Writes go Normal/Thickness/
style -> Rotation -> InsertionPoint re-asserted in WCS when the frame
changed -> Alignment -> TextAlignmentPoint -> Layer -> Color -> Visible.
Two fake-surface tests: the mirrored/styled ATTRIB pins every write and
the Normal-before-Rotation and Normal-before-InsertionPoint order; the
bare ATTRIB pins that absent members are skipped. Both fail on the
previous commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
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
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
Track A's task-1 gate covered block_insert only; the sibling tool still
returned a handle for a typo'd name that doc.audit() later deleted, and
both tools accepted *Model_Space (a reference cycle). One gate per engine
(_require_block / _require_block_defined) is now shared by both placing
tools: non-string name -> TypeError, unknown -> ValueError naming it,
layout block -> ValueError, all before add_blockref / InsertBlock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd