fix(task-2): COM burst MTEXT anchors after the attachment; headless multi-line attribute values persist below R2018

Two review findings on the multi-line-attribute paths of block_explode /
block_set_attributes.

COM: `_add_mtext_from_capture` wrote `InsertionPoint` before
`AttachmentPoint`. ActiveX's `AttachmentPoint` keeps the text body where it
is and relocates the anchor, so every non-TopLeft attribute was laid out
TopLeft-at-anchor with only the label moved (live: a BottomLeft two-line
note landed two lines low, BottomRight also a box width right). The anchor
is now written last, unconditionally, after the attachment and the frame.
Verified live on AutoCAD 2026: every alignment's bbox matches the control
and the end-to-end burst MTEXT sits at the ATTRIB's TextAlignmentPoint.
The fake MTEXT now relocates `InsertionPoint` on `AttachmentPoint`/`Normal`
writes the way ActiveX does, both tests assert where the anchor ended up,
and the alignment test uses BottomRight rather than the default TopLeft
that could never show the displacement.

ezdxf: `_write_attrib_value` relied on `set_mtext`, which mirrors only the
first line into `dxf.text`; below R2018 ezdxf exports no embedded MTEXT,
so the default R2010 document saved `NEW ONE` where the reader had
promised `NEW ONE\PNEW TWO\PNEW THREE`. `dxf.text` now carries the whole
value (AutoCAD's own `TextString` convention) after the MTEXT rebuild;
the R2010 round-trip test uses a `\P` value, and an R2018 round trip
pins both surfaces.

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 15:24:44 +03:00
co-authored by Claude Opus 5
parent e7dafb85ab
commit 5f33613460
3 changed files with 95 additions and 24 deletions
+12 -7
View File
@@ -3471,11 +3471,17 @@ class ComBackend(AutoCADBackend):
ATTRIB's ``TextAlignmentPoint`` when the attribute is box-aligned and
the ``InsertionPoint`` otherwise (ezdxf writes both from the embedded
MTEXT's insert, so they agree). Height, style and frame go on before
``Rotation``, the WCS anchor is re-asserted after a frame change, and
the attachment is the ATTRIB's ``Alignment`` mapped through
``_TEXT_ALIGNMENT_TO_MTEXT_ATTACHMENT``. ``Thickness``, oblique, width
factor and the generation flags are TEXT-only members and stay behind.
Unit-tested against the fake; not yet executed live.
``Rotation``, then the attachment (the ATTRIB's ``Alignment`` mapped
through ``_TEXT_ALIGNMENT_TO_MTEXT_ATTACHMENT``), and the WCS anchor
is written *last*: ActiveX ``AttachmentPoint`` keeps the text body
where it is and relocates ``InsertionPoint`` to the new corner, so an
anchor written before it left every non-TopLeft attribute laid out
TopLeft-at-anchor with only the label moved (verified live on AutoCAD
2026: a BottomLeft two-line note landed two lines low; re-asserting
the anchor after the attachment moves the body). A frame change
(``Normal``) moves the OCS origin the same way, so the one late write
covers both. ``Thickness``, oblique, width factor and the generation
flags are TEXT-only members and stay behind.
"""
members = item["members"]
anchor = item["insertion"]
@@ -3490,11 +3496,10 @@ class ComBackend(AutoCADBackend):
value = members["Normal"]
mtext.Normal = _apoint(value[0], value[1], value[2] if len(value) > 2 else 0.0)
mtext.Rotation = item["rotation"]
if "Normal" in members:
mtext.InsertionPoint = point # the frame moved the OCS origin; the anchor was WCS
mtext.AttachmentPoint = cls._TEXT_ALIGNMENT_TO_MTEXT_ATTACHMENT.get(
members.get("Alignment", 0), 1
)
mtext.InsertionPoint = point # after the attachment and the frame: both relocate it
mtext.Layer = item["layer"]
if "Color" in members:
mtext.Color = members["Color"]
+15 -6
View File
@@ -4764,8 +4764,9 @@ class EzdxfBackend(AutoCADBackend):
"""The value an ATTRIB carries, as AutoCAD's ``TextString`` reports it.
A multi-line attribute keeps its content in an embedded MTEXT and its
``dxf.text`` is only the first line when ezdxf authored it (AutoCAD
writes ``Line1\\PLine2``), so the embedded content is the value; a
``dxf.text`` is only the first line when ezdxf's ``embed_mtext`` /
``set_mtext`` authored it (AutoCAD, and ``_write_attrib_value``, write
``Line1\\PLine2``), so the embedded content is the value; a
single-line attribute's value is ``dxf.text``.
"""
if attrib.has_embedded_mtext_entity:
@@ -4782,15 +4783,23 @@ class EzdxfBackend(AutoCADBackend):
(no embedded MTEXT exported) a save/reload carried the new value the
exploded drawing never showed. The embedded MTEXT is rebuilt from its
own virtual entity with the new content -- the round trip ezdxf's
``BaseAttrib.transform`` itself uses -- so ``dxf.text`` (first line),
the MTEXT and the placement stay one attribute.
``BaseAttrib.transform`` itself uses -- so the MTEXT and the placement
stay one attribute.
``set_mtext`` then mirrors only the *first line* into ``dxf.text``
(ezdxf's own choice), and below R2018 ezdxf exports no embedded MTEXT,
so the default (R2010) document saved ``NEW ONE`` where the reader had
promised ``NEW ONE\\PNEW TWO``. ``dxf.text`` is therefore written last
with the whole value: that is AutoCAD's own convention (``TextString``
of a live multi-line attribute reads ``'LINE ONE\\PLINE TWO'``), the
reader prefers the embedded MTEXT when there is one, and the file
carries every line on every DXF version.
"""
if attrib.has_embedded_mtext_entity:
mtext = attrib.virtual_mtext_entity()
mtext.text = value
attrib.set_mtext(mtext, graphic_properties=False)
else:
attrib.dxf.text = value
attrib.dxf.text = value
async def block_get_attributes(self, handle) -> dict:
def _sync():