fix(task-2): headless block_set_attributes rewrites the embedded MTEXT; block_explode refuses members ezdxf would silently drop
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
This commit is contained in:
co-authored by
Claude Opus 5
parent
6cb7a13c66
commit
e7dafb85ab
@@ -858,6 +858,9 @@ class ComBackend(AutoCADBackend):
|
|||||||
False, reason="window_capture_has_no_render_to_label"
|
False, reason="window_capture_has_no_render_to_label"
|
||||||
),
|
),
|
||||||
"measure_area_acis": FeatureCapability(True, "native"),
|
"measure_area_acis": FeatureCapability(True, "native"),
|
||||||
|
"explode_opaque_members": FeatureCapability(
|
||||||
|
True, "native", reason="autocad_explode;ole_and_proxy_members_unverified_live"
|
||||||
|
),
|
||||||
"ocs_normalized": FeatureCapability(
|
"ocs_normalized": FeatureCapability(
|
||||||
True,
|
True,
|
||||||
"activex_wcs",
|
"activex_wcs",
|
||||||
|
|||||||
@@ -1027,6 +1027,13 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
"measure_area_acis": FeatureCapability(
|
"measure_area_acis": FeatureCapability(
|
||||||
False, reason="acis_evaluation_requires_live_autocad"
|
False, reason="acis_evaluation_requires_live_autocad"
|
||||||
),
|
),
|
||||||
|
"explode_opaque_members": FeatureCapability(
|
||||||
|
False,
|
||||||
|
reason=(
|
||||||
|
"ezdxf_cannot_transform:OLE2FRAME,VIEWPORT,"
|
||||||
|
"ACAD_PROXY_ENTITY_without_proxy_graphic;refused_before_writing"
|
||||||
|
),
|
||||||
|
),
|
||||||
"ocs_normalized": FeatureCapability(
|
"ocs_normalized": FeatureCapability(
|
||||||
True,
|
True,
|
||||||
"wcs",
|
"wcs",
|
||||||
@@ -4631,6 +4638,45 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
"multi-insert cannot be exploded"
|
"multi-insert cannot be exploded"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _explodable_members(ent, handle: str) -> list:
|
||||||
|
"""The reference's members as virtual entities, or a typed refusal
|
||||||
|
before any write when ezdxf would drop one.
|
||||||
|
|
||||||
|
``virtual_entities()`` skips every member it cannot copy (OLE2FRAME)
|
||||||
|
or transform (VIEWPORT) with a debug-level log only, and an
|
||||||
|
ACAD_PROXY_ENTITY contributes its proxy graphic -- nothing at all when
|
||||||
|
it carries none -- so a title block's OLE logo or a vertical product's
|
||||||
|
proxy geometry used to vanish with ``ok: True`` and a clean audit. The
|
||||||
|
list is materialized here so the skips are known before the layout is
|
||||||
|
touched; AutoCAD explodes all of these natively.
|
||||||
|
"""
|
||||||
|
skipped: list[str] = []
|
||||||
|
|
||||||
|
def _collect(entity, reason):
|
||||||
|
entity_handle = entity.dxf.get("handle") or "?"
|
||||||
|
skipped.append(f"{entity.dxftype()} {entity_handle} ({reason})")
|
||||||
|
|
||||||
|
members = list(ent.virtual_entities(skipped_entity_callback=_collect))
|
||||||
|
block = ent.block()
|
||||||
|
for member in block if block is not None else ():
|
||||||
|
if member.dxftype() != "ACAD_PROXY_ENTITY":
|
||||||
|
continue
|
||||||
|
if not any(True for _ in member.virtual_entities()):
|
||||||
|
skipped.append(f"ACAD_PROXY_ENTITY {member.dxf.handle} (no proxy graphic)")
|
||||||
|
if skipped:
|
||||||
|
name = ent.dxf.get("name", "?")
|
||||||
|
raise UnsupportedCapabilityError(
|
||||||
|
"explode_opaque_members",
|
||||||
|
f"block_explode: entity {handle} references block {name!r} whose "
|
||||||
|
f"members {', '.join(skipped)} cannot be exploded by the headless "
|
||||||
|
"ezdxf backend (it cannot transform them; exploding would delete "
|
||||||
|
"them). Nothing was written. Switch to the live COM backend "
|
||||||
|
"(AUTOCAD_MCP_BACKEND=com, needs Windows + AutoCAD), which hands "
|
||||||
|
"the explode to AutoCAD.",
|
||||||
|
)
|
||||||
|
return members
|
||||||
|
|
||||||
async def block_explode(self, handle) -> dict:
|
async def block_explode(self, handle) -> dict:
|
||||||
"""Explode an INSERT into its members; ATTRIB values survive as TEXT.
|
"""Explode an INSERT into its members; ATTRIB values survive as TEXT.
|
||||||
|
|
||||||
@@ -4667,7 +4713,12 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
it there would redefine the block under every other reference), an
|
it there would redefine the block under every other reference), an
|
||||||
xref, and a MINSERT -- see ``_refuse_unexplodable_reference`` for the
|
xref, and a MINSERT -- see ``_refuse_unexplodable_reference`` for the
|
||||||
last two, which ``virtual_entities()`` cannot represent and which used
|
last two, which ``virtual_entities()`` cannot represent and which used
|
||||||
to be deleted with ``ok: True``.
|
to be deleted with ``ok: True`` -- and a reference whose definition
|
||||||
|
holds a member ezdxf cannot copy or transform (OLE2FRAME, VIEWPORT, a
|
||||||
|
proxy entity without proxy graphics), refused by member with the
|
||||||
|
capability key ``explode_opaque_members`` -- see
|
||||||
|
``_explodable_members``; those used to be dropped from the result with
|
||||||
|
``ok: True`` while the reference was deleted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _sync():
|
def _sync():
|
||||||
@@ -4676,8 +4727,9 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
raise RuntimeError(f"Entity {handle} is not a block reference (INSERT)")
|
raise RuntimeError(f"Entity {handle} is not a block reference (INSERT)")
|
||||||
self._refuse_unexplodable_reference(ent, handle)
|
self._refuse_unexplodable_reference(ent, handle)
|
||||||
msp = self._owner_layout_for_explode(ent, handle)
|
msp = self._owner_layout_for_explode(ent, handle)
|
||||||
|
members = self._explodable_members(ent, handle)
|
||||||
inserted = []
|
inserted = []
|
||||||
for sub in ent.virtual_entities():
|
for sub in members:
|
||||||
sub_copy = sub.copy()
|
sub_copy = sub.copy()
|
||||||
msp.add_entity(sub_copy)
|
msp.add_entity(sub_copy)
|
||||||
inserted.append(sub_copy.dxf.handle)
|
inserted.append(sub_copy.dxf.handle)
|
||||||
@@ -4707,6 +4759,39 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
|
|
||||||
return await self._async(_sync)
|
return await self._async(_sync)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _attrib_value(attrib) -> str:
|
||||||
|
"""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
|
||||||
|
single-line attribute's value is ``dxf.text``.
|
||||||
|
"""
|
||||||
|
if attrib.has_embedded_mtext_entity:
|
||||||
|
return attrib.virtual_mtext_entity().text
|
||||||
|
return attrib.dxf.text
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _write_attrib_value(attrib, value: str) -> None:
|
||||||
|
"""Write ``value`` into ``attrib`` on every surface it has.
|
||||||
|
|
||||||
|
Setting only ``dxf.text`` on a multi-line ATTRIB left the embedded
|
||||||
|
MTEXT at its old content: ``block_get_attributes`` reported the new
|
||||||
|
value, ``block_explode`` burst the old one, and on an R2010 document
|
||||||
|
(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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
async def block_get_attributes(self, handle) -> dict:
|
async def block_get_attributes(self, handle) -> dict:
|
||||||
def _sync():
|
def _sync():
|
||||||
ent = self._get_entity(handle)
|
ent = self._get_entity(handle)
|
||||||
@@ -4714,7 +4799,7 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
raise RuntimeError(f"Entity {handle} is not a block reference")
|
raise RuntimeError(f"Entity {handle} is not a block reference")
|
||||||
result = {}
|
result = {}
|
||||||
for attrib in ent.attribs:
|
for attrib in ent.attribs:
|
||||||
result[attrib.dxf.tag] = attrib.dxf.text
|
result[attrib.dxf.tag] = self._attrib_value(attrib)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return await self._async(_sync)
|
return await self._async(_sync)
|
||||||
@@ -4728,7 +4813,7 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
for attrib in ent.attribs:
|
for attrib in ent.attribs:
|
||||||
tag = attrib.dxf.tag
|
tag = attrib.dxf.tag
|
||||||
if tag in attributes:
|
if tag in attributes:
|
||||||
attrib.dxf.text = str(attributes[tag])
|
self._write_attrib_value(attrib, str(attributes[tag]))
|
||||||
updated.append(tag)
|
updated.append(tag)
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
return {"ok": True, "updated_tags": updated}
|
return {"ok": True, "updated_tags": updated}
|
||||||
|
|||||||
@@ -3040,8 +3040,12 @@ async def block_explode(
|
|||||||
(one TEXT or MTEXT handle per attribute), `exploded_handle` and
|
(one TEXT or MTEXT handle per attribute), `exploded_handle` and
|
||||||
`backend`. Refused, with nothing written: a handle that is not a block
|
`backend`. Refused, with nothing written: a handle that is not a block
|
||||||
reference, a reference nested inside a block definition (explode the
|
reference, a reference nested inside a block definition (explode the
|
||||||
outer reference instead), an external reference (bind it first), and a
|
outer reference instead), an external reference (bind it first), a
|
||||||
MINSERT grid. Not undoable except through `drawing_undo` / a transaction.
|
MINSERT grid, and -- headless only, capability `explode_opaque_members`
|
||||||
|
-- a block holding a member ezdxf cannot transform (an OLE2FRAME logo, a
|
||||||
|
VIEWPORT, a proxy entity without proxy graphics), named by member; the
|
||||||
|
live engine hands those to AutoCAD. Not undoable except through
|
||||||
|
`drawing_undo` / a transaction.
|
||||||
"""
|
"""
|
||||||
await ctx.warning(f"Exploding block reference {handle}")
|
await ctx.warning(f"Exploding block reference {handle}")
|
||||||
return await _backend(ctx).block_explode(handle)
|
return await _backend(ctx).block_explode(handle)
|
||||||
@@ -3056,7 +3060,11 @@ async def block_get_attributes(
|
|||||||
handle: Annotated[str, "Block reference (INSERT) entity handle"],
|
handle: Annotated[str, "Block reference (INSERT) entity handle"],
|
||||||
ctx: Context = None,
|
ctx: Context = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get all attribute values from a block reference as {TAG: value} dict."""
|
"""Get all attribute values from a block reference as {TAG: value} dict.
|
||||||
|
|
||||||
|
A multi-line attribute reports its whole content with `\\P` between the
|
||||||
|
lines (AutoCAD's `TextString`), on both engines.
|
||||||
|
"""
|
||||||
return await _backend(ctx).block_get_attributes(handle)
|
return await _backend(ctx).block_get_attributes(handle)
|
||||||
|
|
||||||
|
|
||||||
@@ -3073,7 +3081,12 @@ async def block_set_attributes(
|
|||||||
attributes: Annotated[dict, "Attribute values to update: {TAG: new_value}"],
|
attributes: Annotated[dict, "Attribute values to update: {TAG: new_value}"],
|
||||||
ctx: Context = None,
|
ctx: Context = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Update attribute values in a block reference."""
|
"""Update attribute values in a block reference.
|
||||||
|
|
||||||
|
A multi-line attribute takes `Line1\\PLine2` and keeps every line; the
|
||||||
|
value written is the one `block_get_attributes` reads back and
|
||||||
|
`block_explode` bursts, on both engines.
|
||||||
|
"""
|
||||||
return await _backend(ctx).block_set_attributes(handle, attributes)
|
return await _backend(ctx).block_set_attributes(handle, attributes)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1075,3 +1075,157 @@ async def test_com_explode_refuses_a_minsert_by_name_before_explode(com):
|
|||||||
with pytest.raises(RuntimeError, match=r"MINSERT \(2x3 grid of 'CELL'\)"):
|
with pytest.raises(RuntimeError, match=r"MINSERT \(2x3 grid of 'CELL'\)"):
|
||||||
await backend.block_explode("9B")
|
await backend.block_explode("9B")
|
||||||
assert dispatched == [] and space.calls == [] and grid.deleted is False
|
assert dispatched == [] and space.calls == [] and grid.deleted is False
|
||||||
|
|
||||||
|
|
||||||
|
# ── Task 2 (review 2): the multi-line writer, and members ezdxf cannot explode ─
|
||||||
|
|
||||||
|
|
||||||
|
async def _insert_multi_line(backend):
|
||||||
|
"""An INSERT of a LINE + ATTDEF block whose ATTRIB is multi-line (embedded
|
||||||
|
MTEXT ``OLD ONE\\POLD TWO``), built the way the tests above build one."""
|
||||||
|
blk = backend._doc.blocks.new(name="MLW")
|
||||||
|
blk.add_line((0, 0), (10, 0))
|
||||||
|
blk.add_attdef("NOTE", insert=(0, 3), text="x", dxfattribs={"height": 2.5})
|
||||||
|
ref = await backend.block_insert("MLW", 10, 10, rotation=30.0, attributes={"NOTE": "first"})
|
||||||
|
attrib = backend._doc.entitydb[ref.handle].attribs[0]
|
||||||
|
_embed_multi_line(attrib, ["OLD ONE", "OLD TWO"], attrib.dxf.insert)
|
||||||
|
return ref, attrib
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_attributes_reports_every_line_of_a_multi_line_attrib(backend):
|
||||||
|
"""``dxf.text`` of an ezdxf-embedded multi-line ATTRIB is only the first
|
||||||
|
line; AutoCAD's ``TextString`` (what the COM engine reports) is the whole
|
||||||
|
``Line1\\PLine2`` content, and so is the headless reader now."""
|
||||||
|
ref, _ = await _insert_multi_line(backend)
|
||||||
|
assert await backend.block_get_attributes(ref.handle) == {"NOTE": "OLD ONE\\POLD TWO"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_attributes_rewrites_the_embedded_mtext_of_a_multi_line_attrib(backend):
|
||||||
|
"""The writer used to set only ``dxf.text``, so after an edit the reader
|
||||||
|
said ``NEW VALUE`` while the explode burst the pre-edit MTEXT -- and an
|
||||||
|
R2010 save (no embedded MTEXT exported) carried a value the exploded
|
||||||
|
drawing never showed. One value, on every surface."""
|
||||||
|
ref, attrib = await _insert_multi_line(backend)
|
||||||
|
keys = ("insert", "align_point", "rotation", "height", "halign", "valign", "style")
|
||||||
|
before = {key: attrib.dxf.get(key) for key in keys}
|
||||||
|
|
||||||
|
result = await backend.block_set_attributes(ref.handle, {"NOTE": "NEW VALUE"})
|
||||||
|
|
||||||
|
assert result == {"ok": True, "updated_tags": ["NOTE"]}
|
||||||
|
assert attrib.has_embedded_mtext_entity, "still a multi-line attribute"
|
||||||
|
assert attrib.virtual_mtext_entity().text == "NEW VALUE"
|
||||||
|
assert attrib.dxf.text == "NEW VALUE"
|
||||||
|
after = {key: attrib.dxf.get(key) for key in keys}
|
||||||
|
assert after == before, "rewriting the content does not move the attribute"
|
||||||
|
assert await backend.block_get_attributes(ref.handle) == {"NOTE": "NEW VALUE"}
|
||||||
|
exploded = await backend.block_explode(ref.handle)
|
||||||
|
(handle,) = exploded["attribute_texts"]
|
||||||
|
raw = backend._doc.entitydb[handle]
|
||||||
|
assert raw.dxftype() == "MTEXT" and raw.text == "NEW VALUE"
|
||||||
|
assert backend._doc.audit().errors == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_attributes_multi_line_value_keeps_every_line(backend):
|
||||||
|
"""A ``\\P`` value on a multi-line attribute is what AutoCAD's
|
||||||
|
``TextString`` write takes; every line survives, and the empty value
|
||||||
|
empties the attribute rather than leaving the old content behind."""
|
||||||
|
ref, attrib = await _insert_multi_line(backend)
|
||||||
|
|
||||||
|
await backend.block_set_attributes(ref.handle, {"NOTE": "NEW ONE\\PNEW TWO\\PNEW THREE"})
|
||||||
|
|
||||||
|
mtext = attrib.virtual_mtext_entity()
|
||||||
|
assert mtext.plain_text(split=True) == ["NEW ONE", "NEW TWO", "NEW THREE"]
|
||||||
|
assert attrib.dxf.text == "NEW ONE", "ezdxf's single-line mirror is the first line"
|
||||||
|
assert await backend.block_get_attributes(ref.handle) == {
|
||||||
|
"NOTE": "NEW ONE\\PNEW TWO\\PNEW THREE"
|
||||||
|
}
|
||||||
|
|
||||||
|
await backend.block_set_attributes(ref.handle, {"NOTE": ""})
|
||||||
|
assert await backend.block_get_attributes(ref.handle) == {"NOTE": ""}
|
||||||
|
assert attrib.virtual_mtext_entity().text == "" and attrib.dxf.text == ""
|
||||||
|
|
||||||
|
exploded = await backend.block_explode(ref.handle)
|
||||||
|
(handle,) = exploded["attribute_texts"]
|
||||||
|
assert backend._doc.entitydb[handle].text == ""
|
||||||
|
assert backend._doc.audit().errors == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_attributes_on_a_multi_line_attrib_survives_an_r2010_round_trip(
|
||||||
|
backend, tmp_path
|
||||||
|
):
|
||||||
|
"""The default document is R2010, where ezdxf exports no embedded MTEXT:
|
||||||
|
the saved file carries only ``dxf.text``. After the fix that is the
|
||||||
|
edited value, so the reloaded drawing and the exploded one agree."""
|
||||||
|
ref, _ = await _insert_multi_line(backend)
|
||||||
|
assert backend._doc.dxfversion == "AC1024", "the premise"
|
||||||
|
await backend.block_set_attributes(ref.handle, {"NOTE": "NEW VALUE"})
|
||||||
|
path = tmp_path / "ml.dxf"
|
||||||
|
await backend.drawing_save_as(str(path))
|
||||||
|
await backend.drawing_open(str(path))
|
||||||
|
|
||||||
|
reloaded = next(e for e in backend._msp() if e.dxftype() == "INSERT")
|
||||||
|
assert not reloaded.attribs[0].has_embedded_mtext_entity, "R2010 drops the embedded MTEXT"
|
||||||
|
assert await backend.block_get_attributes(reloaded.dxf.handle) == {"NOTE": "NEW VALUE"}
|
||||||
|
exploded = await backend.block_explode(reloaded.dxf.handle)
|
||||||
|
(handle,) = exploded["attribute_texts"]
|
||||||
|
raw = backend._doc.entitydb[handle]
|
||||||
|
assert raw.dxftype() == "TEXT" and raw.dxf.text == "NEW VALUE"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_attributes_leaves_a_single_line_attrib_single_line(backend):
|
||||||
|
_define_tagged_block(backend, "SL")
|
||||||
|
ref = await backend.block_insert("SL", 0, 0, attributes={"TAG": "P-1"})
|
||||||
|
await backend.block_set_attributes(ref.handle, {"TAG": "P-2"})
|
||||||
|
attrib = backend._doc.entitydb[ref.handle].attribs[0]
|
||||||
|
assert not attrib.has_embedded_mtext_entity and attrib.dxf.text == "P-2"
|
||||||
|
assert await backend.block_get_attributes(ref.handle) == {"TAG": "P-2"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_explode_refuses_members_ezdxf_cannot_explode_before_writing(backend):
|
||||||
|
"""``virtual_entities()`` drops every member it cannot copy or transform
|
||||||
|
(OLE2FRAME, VIEWPORT) with a debug log, and a proxy entity without proxy
|
||||||
|
graphics contributes nothing -- so a title block's OLE logo or a vertical
|
||||||
|
product's proxy geometry used to be destroyed with ``ok: True`` and a
|
||||||
|
clean audit. Refused by member before anything is written."""
|
||||||
|
from ezdxf.entities import factory
|
||||||
|
|
||||||
|
from backends.base import UnsupportedCapabilityError
|
||||||
|
|
||||||
|
blk = backend._doc.blocks.new(name="PRX")
|
||||||
|
blk.add_line((0, 0), (10, 0))
|
||||||
|
for dxftype in ("ACAD_PROXY_ENTITY", "OLE2FRAME", "VIEWPORT"):
|
||||||
|
blk.add_entity(factory.create_db_entry(dxftype, {}, backend._doc))
|
||||||
|
ref = await backend.block_insert("PRX", 0, 0)
|
||||||
|
before = [e.dxf.handle for e in backend._msp()]
|
||||||
|
|
||||||
|
with pytest.raises(UnsupportedCapabilityError) as excinfo:
|
||||||
|
await backend.block_explode(ref.handle)
|
||||||
|
|
||||||
|
assert excinfo.value.capability == "explode_opaque_members"
|
||||||
|
message = str(excinfo.value)
|
||||||
|
for name in ("ACAD_PROXY_ENTITY", "OLE2FRAME", "VIEWPORT", "'PRX'", "com"):
|
||||||
|
assert name in message, message
|
||||||
|
assert [e.dxf.handle for e in backend._msp()] == before, "nothing written"
|
||||||
|
assert backend._doc.entitydb[ref.handle].is_alive
|
||||||
|
|
||||||
|
|
||||||
|
async def test_explode_still_bursts_a_block_every_member_of_which_it_can_explode(backend):
|
||||||
|
"""The refusal is per member, not per block: ordinary geometry keeps
|
||||||
|
exploding, in definition order, under a uniform scale."""
|
||||||
|
blk = backend._doc.blocks.new(name="PLAIN2")
|
||||||
|
blk.add_line((0, 0), (10, 0))
|
||||||
|
blk.add_circle((5, 5), 1)
|
||||||
|
blk.add_hatch().paths.add_polyline_path([(0, 0), (1, 0), (1, 1)])
|
||||||
|
ref = await backend.block_insert("PLAIN2", 0, 0, scale_x=2.0, scale_y=2.0)
|
||||||
|
result = await backend.block_explode(ref.handle)
|
||||||
|
types = [(await backend.entity_get(h)).type for h in result["inserted_handles"]]
|
||||||
|
assert types == ["LINE", "CIRCLE", "HATCH"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_explode_opaque_members_is_declared_on_both_engines():
|
||||||
|
from backends.com_backend import ComBackend
|
||||||
|
from backends.ezdxf_backend import EzdxfBackend
|
||||||
|
|
||||||
|
headless = EzdxfBackend().capabilities().features["explode_opaque_members"]
|
||||||
|
assert headless.supported is False and "OLE2FRAME" in (headless.reason or "")
|
||||||
|
assert ComBackend().capabilities().features["explode_opaque_members"].supported is True
|
||||||
|
|||||||
Reference in New Issue
Block a user