fix(com): four live defects the settings smoke measured on AutoCAD 2026 — rejected calls after a document switch, typelib spelling of color/Lineweight, narrowed layer-state dictionary, layer 0 Freeze put
Measured 2026-09-22 by scripts/smoke_settings_com.py --build-dwt against the running AutoCAD 2026 (25.1s), each on a scratch Documents.Add() document: 1. RPC_E_CALL_REJECTED (-2147418111) on Documents.Count straight after Documents.Open, after Close(False) inside document_close, and in _ensure_document_state: AutoCAD's own message filter refuses incoming calls while it is still switching documents. pywin32 exposes no CoRegisterMessageFilter, so _wait_out_rejected_call waits the window out for read-only callables (0.25 s -> 1 s pauses, 20 s budget, inside COM_CALL_TIMEOUT) at those sites and after Documents.Add/Open; any other com_error is not retried. 2. layer_list raised "'IAcadLayer' object has no attribute 'Color'. Did you mean: 'color'?": acax25enu.tlb spells IAcadEntity/IAcadLayer `color` and `Lineweight`. Late-bound dispatch resolved .Color/.LineWeight case-insensitively; with a makepy cache present (this machine's gen_py was regenerated on 2026-09-17, and every object a method returns is then a generated wrapper) the lookup is exact. All 17 sites now use the typelib spelling, which works on both wrappers; the fakes model the measured names. 3. layer_state_restore raised "'IAcadObject' object has no attribute 'Count'": Dictionaries.Item and IAcadDictionary.Item are declared IAcadObject* (the narrowing _com_unnarrow already handles for MLEADER styles). The save path went through the typed Dictionaries.Add, so only the reopen paths failed. Both now un-narrow; the fakes narrow like pywin32. 4. The Freeze put on layer 0 raises 'Invalid layer' whatever the value, even False while thawed (color/Linetype/Lineweight/Plottable/LayerOn/Lock/ ActiveLayer all fine on it), so a state saved with 0 current restored with two warnings and 0 unapplied. Freeze is written only when it changes the layer. 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
0514f8f15d
commit
b54271f4ac
+132
-38
@@ -75,6 +75,39 @@ else:
|
|||||||
#: than the bug: it would silently swallow unrelated errors.
|
#: than the bug: it would silently swallow unrelated errors.
|
||||||
_COM_ERROR: tuple[type[BaseException], ...] = (pywintypes.com_error,) if _COM_IMPORTS_OK else ()
|
_COM_ERROR: tuple[type[BaseException], ...] = (pywintypes.com_error,) if _COM_IMPORTS_OK else ()
|
||||||
|
|
||||||
|
#: ``RPC_E_CALL_REJECTED``: the callee's message filter refused the incoming
|
||||||
|
#: call *before* running it, so nothing happened on the seat. AutoCAD answers
|
||||||
|
#: it for a moment after ``Documents.Open`` / ``Add`` / ``Close`` while it is
|
||||||
|
#: still switching documents — measured twice on 2026-09-22 (AutoCAD 2026,
|
||||||
|
#: ``scripts/smoke_settings_com.py --build-dwt``: ``Documents.Count`` right
|
||||||
|
#: after ``Documents.Open`` and right after ``Close(False)``) — and for as long
|
||||||
|
#: as an operator or another client leaves a command prompt open. pywin32 has
|
||||||
|
#: no ``CoRegisterMessageFilter``, the client-side hook that would retry this
|
||||||
|
#: per call, so the read-only reads that follow a switch wait it out themselves.
|
||||||
|
_RPC_E_CALL_REJECTED = -2147418111
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_out_rejected_call(read: Callable[[], Any], *, budget_s: float, first_pause_s: float):
|
||||||
|
"""Run a read-only COM callable again while it answers ``RPC_E_CALL_REJECTED``.
|
||||||
|
|
||||||
|
Only for callables that change nothing on the seat (the rejection means
|
||||||
|
the refused call did not run, but an earlier call inside ``read`` did).
|
||||||
|
Pauses double from ``first_pause_s`` up to one second and stop at
|
||||||
|
``budget_s``; any other ``com_error`` is re-raised on the first attempt.
|
||||||
|
"""
|
||||||
|
deadline = time.monotonic() + budget_s
|
||||||
|
pause = first_pause_s
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return read()
|
||||||
|
except _COM_ERROR as exc:
|
||||||
|
hr = exc.args[0] if exc.args else 0
|
||||||
|
if hr != _RPC_E_CALL_REJECTED or time.monotonic() + pause > deadline:
|
||||||
|
raise
|
||||||
|
time.sleep(pause)
|
||||||
|
pause = min(pause * 2, 1.0)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
@@ -440,7 +473,7 @@ def _apply_entity_attrs(entity, layer: str | None, color: int | None, linetype:
|
|||||||
if layer is not None:
|
if layer is not None:
|
||||||
entity.Layer = layer
|
entity.Layer = layer
|
||||||
if color is not None:
|
if color is not None:
|
||||||
entity.Color = int(color)
|
entity.color = int(color)
|
||||||
if linetype is not None:
|
if linetype is not None:
|
||||||
_ensure_linetype_loaded(linetype)
|
_ensure_linetype_loaded(linetype)
|
||||||
entity.Linetype = linetype
|
entity.Linetype = linetype
|
||||||
@@ -1286,20 +1319,34 @@ def _entity_info(entity) -> EntityInfo:
|
|||||||
handle=entity.Handle,
|
handle=entity.Handle,
|
||||||
type=ent_type,
|
type=ent_type,
|
||||||
layer=entity.Layer,
|
layer=entity.Layer,
|
||||||
color=entity.Color,
|
color=entity.color,
|
||||||
linetype=linetype,
|
linetype=linetype,
|
||||||
visible=bool(entity.Visible),
|
visible=bool(entity.Visible),
|
||||||
properties=props,
|
properties=props,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Two ActiveX members this backend spells the way the type library does and
|
||||||
|
#: not the way the documentation capitalises them: ``IAcadEntity`` / ``IAcadLayer``
|
||||||
|
#: declare ``color`` and ``Lineweight`` (acax25enu.tlb). Late-bound dispatch
|
||||||
|
#: resolves names case-insensitively, so ``.Color`` worked on a seat without a
|
||||||
|
#: makepy cache; once ``win32com.client.gencache`` has run against AutoCAD
|
||||||
|
#: (any tool calling ``EnsureDispatch`` leaves it behind, and every object a
|
||||||
|
#: method returns is then wrapped in the generated class) the lookup is exact
|
||||||
|
#: and ``.Color`` / ``.LineWeight`` raise ``AttributeError: ... no attribute
|
||||||
|
#: 'Color'. Did you mean: 'color'?`` — measured 2026-09-22 on AutoCAD 2026 by
|
||||||
|
#: ``scripts/smoke_settings_com.py`` (``layer_list`` on the reopened template)
|
||||||
|
#: and on a scratch document's ``AddLine``. The typelib spelling works on both
|
||||||
|
#: kinds of wrapper; the fakes in tests/ model the same names.
|
||||||
|
|
||||||
|
|
||||||
def _layer_info(layer, current_layer_name: str) -> LayerInfo:
|
def _layer_info(layer, current_layer_name: str) -> LayerInfo:
|
||||||
"""Convert a COM layer object to LayerInfo."""
|
"""Convert a COM layer object to LayerInfo."""
|
||||||
return LayerInfo(
|
return LayerInfo(
|
||||||
name=layer.Name,
|
name=layer.Name,
|
||||||
color=layer.Color,
|
color=layer.color,
|
||||||
linetype=layer.Linetype,
|
linetype=layer.Linetype,
|
||||||
lineweight=layer.LineWeight,
|
lineweight=layer.Lineweight,
|
||||||
is_on=bool(layer.LayerOn),
|
is_on=bool(layer.LayerOn),
|
||||||
is_frozen=bool(layer.Freeze),
|
is_frozen=bool(layer.Freeze),
|
||||||
is_locked=bool(layer.Lock),
|
is_locked=bool(layer.Lock),
|
||||||
@@ -1397,6 +1444,11 @@ def _capture_window(hwnd: int) -> bytes | None:
|
|||||||
class ComBackend(AutoCADBackend):
|
class ComBackend(AutoCADBackend):
|
||||||
"""Live AutoCAD control via COM API."""
|
"""Live AutoCAD control via COM API."""
|
||||||
|
|
||||||
|
#: How long ``_ensure_document_state`` waits out ``RPC_E_CALL_REJECTED``
|
||||||
|
#: after a document switch, well inside ``COM_CALL_TIMEOUT`` (60 s).
|
||||||
|
_REJECTED_RETRY_BUDGET_S = 20.0
|
||||||
|
_REJECTED_RETRY_FIRST_PAUSE_S = 0.25
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._executor: ThreadPoolExecutor | None = None
|
self._executor: ThreadPoolExecutor | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
@@ -1518,16 +1570,32 @@ class ComBackend(AutoCADBackend):
|
|||||||
self._reset_document_state()
|
self._reset_document_state()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
async def _ensure_document_state(self) -> None:
|
def _wait_out_rejected_call(self, read: Callable[[], Any]):
|
||||||
"""Reset drawing-scoped metadata when AutoCAD's active document changes."""
|
"""``_wait_out_rejected_call`` with this backend's budget (COM thread only)."""
|
||||||
|
return _wait_out_rejected_call(
|
||||||
|
read,
|
||||||
|
budget_s=self._REJECTED_RETRY_BUDGET_S,
|
||||||
|
first_pause_s=self._REJECTED_RETRY_FIRST_PAUSE_S,
|
||||||
|
)
|
||||||
|
|
||||||
def _sync():
|
async def _ensure_document_state(self) -> None:
|
||||||
|
"""Reset drawing-scoped metadata when AutoCAD's active document changes.
|
||||||
|
|
||||||
|
Runs directly after ``Documents.Add`` / ``Open`` / ``Close`` — the
|
||||||
|
window in which AutoCAD still rejects incoming calls (see
|
||||||
|
``_RPC_E_CALL_REJECTED``) — so the read waits that window out. It is
|
||||||
|
a pure read, which is what makes re-running it safe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _read_active_key():
|
||||||
app = _acad_app()
|
app = _acad_app()
|
||||||
if app.Documents.Count == 0:
|
if app.Documents.Count == 0:
|
||||||
key = None
|
return None
|
||||||
else:
|
|
||||||
doc = app.ActiveDocument
|
doc = app.ActiveDocument
|
||||||
key = (str(doc.Name), str(doc.FullName))
|
return (str(doc.Name), str(doc.FullName))
|
||||||
|
|
||||||
|
def _sync():
|
||||||
|
key = self._wait_out_rejected_call(_read_active_key)
|
||||||
if key != self._document_scope_key:
|
if key != self._document_scope_key:
|
||||||
self._document_scope_key = key
|
self._document_scope_key = key
|
||||||
self._transaction_active = False
|
self._transaction_active = False
|
||||||
@@ -1678,7 +1746,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
app = _acad_app()
|
app = _acad_app()
|
||||||
if not template:
|
if not template:
|
||||||
doc = app.Documents.Add()
|
doc = app.Documents.Add()
|
||||||
return {"ok": True, "name": doc.Name}
|
return {"ok": True, "name": self._wait_out_rejected_call(lambda: doc.Name)}
|
||||||
source = Path(template)
|
source = Path(template)
|
||||||
if not source.is_file():
|
if not source.is_file():
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
@@ -1692,7 +1760,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
dwt, cached = _template_as_dwt(app, source)
|
dwt, cached = _template_as_dwt(app, source)
|
||||||
result["template_dwt"] = {"path": str(dwt), "cached": cached}
|
result["template_dwt"] = {"path": str(dwt), "cached": cached}
|
||||||
doc = app.Documents.Add(str(dwt))
|
doc = app.Documents.Add(str(dwt))
|
||||||
result["name"] = doc.Name
|
result["name"] = self._wait_out_rejected_call(lambda: doc.Name)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result = await self._run(_sync)
|
result = await self._run(_sync)
|
||||||
@@ -1703,7 +1771,8 @@ class ComBackend(AutoCADBackend):
|
|||||||
def _sync():
|
def _sync():
|
||||||
app = _acad_app()
|
app = _acad_app()
|
||||||
doc = app.Documents.Open(path)
|
doc = app.Documents.Open(path)
|
||||||
return {"ok": True, "name": doc.Name, "path": doc.FullName}
|
name, full_name = self._wait_out_rejected_call(lambda: (doc.Name, doc.FullName))
|
||||||
|
return {"ok": True, "name": name, "path": full_name}
|
||||||
|
|
||||||
result = await self._run(_sync)
|
result = await self._run(_sync)
|
||||||
await self._ensure_document_state()
|
await self._ensure_document_state()
|
||||||
@@ -2127,14 +2196,21 @@ class ComBackend(AutoCADBackend):
|
|||||||
# A clean or untitled document is never asked to save: Close(True)
|
# A clean or untitled document is never asked to save: Close(True)
|
||||||
# on a clean file is a no-op write, on an untitled one a dialog.
|
# on a clean file is a no-op write, on an untitled one a dialog.
|
||||||
doc.Close(bool(save and dirty and path))
|
doc.Close(bool(save and dirty and path))
|
||||||
|
|
||||||
|
def _read_after_close():
|
||||||
|
# AutoCAD still rejects calls for a moment after Close
|
||||||
|
# (see _RPC_E_CALL_REJECTED); this read changes nothing.
|
||||||
count = int(app.Documents.Count)
|
count = int(app.Documents.Count)
|
||||||
|
return count, (str(app.ActiveDocument.Name) if count else None)
|
||||||
|
|
||||||
|
count, active = self._wait_out_rejected_call(_read_after_close)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"closed": name,
|
"closed": name,
|
||||||
"path": path,
|
"path": path,
|
||||||
"saved": bool(save and dirty and path),
|
"saved": bool(save and dirty and path),
|
||||||
"discarded_changes": bool(dirty and not save),
|
"discarded_changes": bool(dirty and not save),
|
||||||
"active": str(app.ActiveDocument.Name) if count else None,
|
"active": active,
|
||||||
"open_documents": count,
|
"open_documents": count,
|
||||||
"backend": "com",
|
"backend": "com",
|
||||||
}
|
}
|
||||||
@@ -2147,15 +2223,25 @@ class ComBackend(AutoCADBackend):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _com_layer_states(doc, *, create: bool):
|
def _com_layer_states(doc, *, create: bool):
|
||||||
|
"""The ``ACADMCP_LAYERSTATES`` dictionary, or None when absent.
|
||||||
|
|
||||||
|
``Dictionaries.Item`` is declared ``IAcadObject*`` (see
|
||||||
|
``_com_unnarrow``): with a makepy cache the wrapper has no ``Count``,
|
||||||
|
``Item`` or ``AddXRecord`` — measured 2026-09-22 on AutoCAD 2026 by
|
||||||
|
``layer_state_restore`` in ``scripts/smoke_settings_com.py`` (the save
|
||||||
|
before it had gone through ``Dictionaries.Add``, which is typed, so
|
||||||
|
only the reopen paths failed). Every object the dictionary hands out
|
||||||
|
goes through ``_com_unnarrow`` for the same reason.
|
||||||
|
"""
|
||||||
from engineering.environment.layer_states import DICT_NAME
|
from engineering.environment.layer_states import DICT_NAME
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return doc.Dictionaries.Item(DICT_NAME)
|
return _com_unnarrow(doc.Dictionaries.Item(DICT_NAME))
|
||||||
except Exception as exc: # absent
|
except Exception as exc: # absent
|
||||||
log.debug("Dictionaries.Item(%s): %s", DICT_NAME, exc)
|
log.debug("Dictionaries.Item(%s): %s", DICT_NAME, exc)
|
||||||
if not create:
|
if not create:
|
||||||
return None
|
return None
|
||||||
return doc.Dictionaries.Add(DICT_NAME)
|
return _com_unnarrow(doc.Dictionaries.Add(DICT_NAME))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _com_dict_entries(states) -> list[tuple[str, Any]]:
|
def _com_dict_entries(states) -> list[tuple[str, Any]]:
|
||||||
@@ -2163,7 +2249,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
return []
|
return []
|
||||||
out = []
|
out = []
|
||||||
for index in range(int(states.Count)):
|
for index in range(int(states.Count)):
|
||||||
obj = states.Item(index)
|
obj = _com_unnarrow(states.Item(index)) # IAcadDictionary.Item is IAcadObject* too
|
||||||
out.append((str(states.GetName(obj)), obj))
|
out.append((str(states.GetName(obj)), obj))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -2261,11 +2347,19 @@ class ComBackend(AutoCADBackend):
|
|||||||
# comes before the ActiveLayer write instead of after it in the
|
# comes before the ActiveLayer write instead of after it in the
|
||||||
# loop. Only a requested property is touched: without "frozen" the
|
# loop. Only a requested property is touched: without "frozen" the
|
||||||
# refusal stands and lands in ``warnings`` like every other one.
|
# refusal stands and lands in ``warnings`` like every other one.
|
||||||
|
# ``Freeze`` is written only when it changes the layer: layer 0
|
||||||
|
# refuses the put with 'Invalid layer' whatever the value, even
|
||||||
|
# ``False`` while thawed (measured 2026-09-22, AutoCAD 2026), and
|
||||||
|
# a state saved with 0 current came back with two warnings.
|
||||||
if "current" in props and state["current_layer"] not in missing:
|
if "current" in props and state["current_layer"] not in missing:
|
||||||
target_name = state["current_layer"]
|
target_name = state["current_layer"]
|
||||||
target = doc.Layers.Item(target_name)
|
target = doc.Layers.Item(target_name)
|
||||||
try:
|
try:
|
||||||
if "frozen" in props and not bool(state["layers"][target_name]["frozen"]):
|
if (
|
||||||
|
"frozen" in props
|
||||||
|
and not bool(state["layers"][target_name]["frozen"])
|
||||||
|
and bool(target.Freeze)
|
||||||
|
):
|
||||||
target.Freeze = False
|
target.Freeze = False
|
||||||
doc.ActiveLayer = target
|
doc.ActiveLayer = target
|
||||||
current = target_name
|
current = target_name
|
||||||
@@ -2278,17 +2372,17 @@ class ComBackend(AutoCADBackend):
|
|||||||
lyr = doc.Layers.Item(layer_name)
|
lyr = doc.Layers.Item(layer_name)
|
||||||
try:
|
try:
|
||||||
if "color" in props:
|
if "color" in props:
|
||||||
lyr.Color = int(snap["color"])
|
lyr.color = int(snap["color"])
|
||||||
if "linetype" in props:
|
if "linetype" in props:
|
||||||
_ensure_linetype_loaded(snap["linetype"])
|
_ensure_linetype_loaded(snap["linetype"])
|
||||||
lyr.Linetype = snap["linetype"]
|
lyr.Linetype = snap["linetype"]
|
||||||
if "lineweight" in props:
|
if "lineweight" in props:
|
||||||
lyr.LineWeight = int(snap["lineweight"])
|
lyr.Lineweight = int(snap["lineweight"])
|
||||||
if "plot" in props:
|
if "plot" in props:
|
||||||
lyr.Plottable = bool(snap["plot"])
|
lyr.Plottable = bool(snap["plot"])
|
||||||
if "on" in props:
|
if "on" in props:
|
||||||
lyr.LayerOn = bool(snap["on"])
|
lyr.LayerOn = bool(snap["on"])
|
||||||
if "frozen" in props:
|
if "frozen" in props and bool(lyr.Freeze) != bool(snap["frozen"]):
|
||||||
lyr.Freeze = bool(snap["frozen"])
|
lyr.Freeze = bool(snap["frozen"])
|
||||||
if "locked" in props:
|
if "locked" in props:
|
||||||
lyr.Lock = bool(snap["locked"])
|
lyr.Lock = bool(snap["locked"])
|
||||||
@@ -3011,7 +3105,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
continue
|
continue
|
||||||
if wanted_layer and entity.Layer != wanted_layer:
|
if wanted_layer and entity.Layer != wanted_layer:
|
||||||
continue
|
continue
|
||||||
if color is not None and int(entity.Color) != int(color):
|
if color is not None and int(entity.color) != int(color):
|
||||||
continue
|
continue
|
||||||
if wanted_linetype and entity.Linetype != wanted_linetype:
|
if wanted_linetype and entity.Linetype != wanted_linetype:
|
||||||
continue
|
continue
|
||||||
@@ -3129,7 +3223,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
"pattern": hatch.PatternName,
|
"pattern": hatch.PatternName,
|
||||||
"scale": float(hatch.PatternScale),
|
"scale": float(hatch.PatternScale),
|
||||||
"angle": float(hatch.PatternAngle),
|
"angle": float(hatch.PatternAngle),
|
||||||
"color": int(hatch.Color),
|
"color": int(hatch.color),
|
||||||
"style": int(hatch.HatchStyle),
|
"style": int(hatch.HatchStyle),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3141,7 +3235,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
if angle is not None:
|
if angle is not None:
|
||||||
hatch.PatternAngle = math.radians(float(angle))
|
hatch.PatternAngle = math.radians(float(angle))
|
||||||
if color is not None:
|
if color is not None:
|
||||||
hatch.Color = int(color)
|
hatch.color = int(color)
|
||||||
if wanted_style:
|
if wanted_style:
|
||||||
hatch.HatchStyle = self._HATCH_STYLES.index(wanted_style)
|
hatch.HatchStyle = self._HATCH_STYLES.index(wanted_style)
|
||||||
hatch.Evaluate()
|
hatch.Evaluate()
|
||||||
@@ -3181,7 +3275,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
dump: dict = {}
|
dump: dict = {}
|
||||||
for member in (
|
for member in (
|
||||||
"Layer",
|
"Layer",
|
||||||
"Color",
|
"color",
|
||||||
"Linetype",
|
"Linetype",
|
||||||
"LinetypeScale",
|
"LinetypeScale",
|
||||||
"Lineweight",
|
"Lineweight",
|
||||||
@@ -4814,12 +4908,12 @@ class ComBackend(AutoCADBackend):
|
|||||||
if layer is not None:
|
if layer is not None:
|
||||||
ent.Layer = layer
|
ent.Layer = layer
|
||||||
if color is not None:
|
if color is not None:
|
||||||
ent.Color = int(color)
|
ent.color = int(color)
|
||||||
if linetype is not None:
|
if linetype is not None:
|
||||||
_ensure_linetype_loaded(linetype)
|
_ensure_linetype_loaded(linetype)
|
||||||
ent.Linetype = linetype
|
ent.Linetype = linetype
|
||||||
if lineweight is not None:
|
if lineweight is not None:
|
||||||
ent.LineWeight = normalize_lineweight(lineweight)
|
ent.Lineweight = normalize_lineweight(lineweight)
|
||||||
if visible is not None:
|
if visible is not None:
|
||||||
ent.Visible = bool(visible)
|
ent.Visible = bool(visible)
|
||||||
return {"ok": True, "handle": handle}
|
return {"ok": True, "handle": handle}
|
||||||
@@ -4999,13 +5093,13 @@ class ComBackend(AutoCADBackend):
|
|||||||
def _sync():
|
def _sync():
|
||||||
doc = _acad_doc()
|
doc = _acad_doc()
|
||||||
lyr = doc.Layers.Add(name)
|
lyr = doc.Layers.Add(name)
|
||||||
lyr.Color = int(color)
|
lyr.color = int(color)
|
||||||
_ensure_linetype_loaded(linetype)
|
_ensure_linetype_loaded(linetype)
|
||||||
try:
|
try:
|
||||||
lyr.Linetype = linetype
|
lyr.Linetype = linetype
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.warning("Failed to set linetype '%s' on layer '%s': %s", linetype, name, exc)
|
log.warning("Failed to set linetype '%s' on layer '%s': %s", linetype, name, exc)
|
||||||
lyr.LineWeight = normalize_lineweight(lineweight)
|
lyr.Lineweight = normalize_lineweight(lineweight)
|
||||||
return _layer_info(lyr, doc.ActiveLayer.Name)
|
return _layer_info(lyr, doc.ActiveLayer.Name)
|
||||||
|
|
||||||
return await self._run(_sync)
|
return await self._run(_sync)
|
||||||
@@ -5039,12 +5133,12 @@ class ComBackend(AutoCADBackend):
|
|||||||
doc = _acad_doc()
|
doc = _acad_doc()
|
||||||
lyr = doc.Layers.Item(name)
|
lyr = doc.Layers.Item(name)
|
||||||
if color is not None:
|
if color is not None:
|
||||||
lyr.Color = int(color)
|
lyr.color = int(color)
|
||||||
if linetype is not None:
|
if linetype is not None:
|
||||||
_ensure_linetype_loaded(linetype)
|
_ensure_linetype_loaded(linetype)
|
||||||
lyr.Linetype = linetype
|
lyr.Linetype = linetype
|
||||||
if lineweight is not None:
|
if lineweight is not None:
|
||||||
lyr.LineWeight = normalize_lineweight(lineweight)
|
lyr.Lineweight = normalize_lineweight(lineweight)
|
||||||
return _layer_info(lyr, doc.ActiveLayer.Name)
|
return _layer_info(lyr, doc.ActiveLayer.Name)
|
||||||
|
|
||||||
return await self._run(_sync)
|
return await self._run(_sync)
|
||||||
@@ -5245,7 +5339,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
"Backward",
|
"Backward",
|
||||||
"UpsideDown",
|
"UpsideDown",
|
||||||
)
|
)
|
||||||
_ATTRIB_TEXT_MEMBERS = _ATTRIB_FRAME_MEMBERS + ("Alignment", "TextAlignmentPoint", "Color")
|
_ATTRIB_TEXT_MEMBERS = _ATTRIB_FRAME_MEMBERS + ("Alignment", "TextAlignmentPoint", "color")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _capture_attrib_text_members(cls, attr) -> dict:
|
def _capture_attrib_text_members(cls, attr) -> dict:
|
||||||
@@ -5269,7 +5363,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
continue
|
continue
|
||||||
if name == "Normal" and ocs.is_wcs_frame(raw):
|
if name == "Normal" and ocs.is_wcs_frame(raw):
|
||||||
continue
|
continue
|
||||||
elif name in ("Alignment", "Color"):
|
elif name in ("Alignment", "color"):
|
||||||
try:
|
try:
|
||||||
raw = int(raw)
|
raw = int(raw)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@@ -5393,8 +5487,8 @@ class ComBackend(AutoCADBackend):
|
|||||||
)
|
)
|
||||||
mtext.InsertionPoint = point # after the attachment and the frame: both relocate it
|
mtext.InsertionPoint = point # after the attachment and the frame: both relocate it
|
||||||
mtext.Layer = item["layer"]
|
mtext.Layer = item["layer"]
|
||||||
if "Color" in members:
|
if "color" in members:
|
||||||
mtext.Color = members["Color"]
|
mtext.color = members["color"]
|
||||||
if item["invisible"]:
|
if item["invisible"]:
|
||||||
mtext.Visible = False
|
mtext.Visible = False
|
||||||
return mtext
|
return mtext
|
||||||
@@ -5433,8 +5527,8 @@ class ComBackend(AutoCADBackend):
|
|||||||
tap = members["TextAlignmentPoint"]
|
tap = members["TextAlignmentPoint"]
|
||||||
text.TextAlignmentPoint = _apoint(tap[0], tap[1], tap[2] if len(tap) > 2 else 0.0)
|
text.TextAlignmentPoint = _apoint(tap[0], tap[1], tap[2] if len(tap) > 2 else 0.0)
|
||||||
text.Layer = item["layer"]
|
text.Layer = item["layer"]
|
||||||
if "Color" in members:
|
if "color" in members:
|
||||||
text.Color = members["Color"]
|
text.color = members["color"]
|
||||||
if item["invisible"]:
|
if item["invisible"]:
|
||||||
text.Visible = False
|
text.Visible = False
|
||||||
return text
|
return text
|
||||||
@@ -5802,7 +5896,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
|
|
||||||
def _style(obj, layer: str) -> None:
|
def _style(obj, layer: str) -> None:
|
||||||
obj.Layer = layer
|
obj.Layer = layer
|
||||||
obj.Color = 0 # acByBlock
|
obj.color = 0 # acByBlock
|
||||||
obj.Linetype = "ByBlock"
|
obj.Linetype = "ByBlock"
|
||||||
|
|
||||||
def _sync():
|
def _sync():
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ def _blockref(name, blocks, insertion=(0.0, 0.0, 0.0), sx=1.0, sy=1.0, rotation=
|
|||||||
Rotation=rotation,
|
Rotation=rotation,
|
||||||
Handle="B1",
|
Handle="B1",
|
||||||
Layer="0",
|
Layer="0",
|
||||||
Color=256,
|
color=256,
|
||||||
Linetype="ByLayer",
|
Linetype="ByLayer",
|
||||||
Visible=True,
|
Visible=True,
|
||||||
**extra,
|
**extra,
|
||||||
|
|||||||
@@ -815,3 +815,95 @@ async def test_com_drawing_close_refuses_the_untitled_save_dialog(com_backend):
|
|||||||
with pytest.raises(ValueError, match="Save dialog"):
|
with pytest.raises(ValueError, match="Save dialog"):
|
||||||
await backend.drawing_close(save=True)
|
await backend.drawing_close(save=True)
|
||||||
assert app.Documents._docs[2].calls == []
|
assert app.Documents._docs[2].calls == []
|
||||||
|
|
||||||
|
|
||||||
|
# ── the busy window after a document switch ──────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class _BusyDocuments(_FakeDocuments):
|
||||||
|
"""``Documents`` that answers ``RPC_E_CALL_REJECTED`` a fixed number of
|
||||||
|
times before ``Count`` works — AutoCAD's own message filter refusing the
|
||||||
|
incoming call while it is still switching documents (measured 2026-09-22
|
||||||
|
by ``scripts/smoke_settings_com.py --build-dwt`` on AutoCAD 2026, twice:
|
||||||
|
``Documents.Count`` right after ``Documents.Open`` and after ``Close``)."""
|
||||||
|
|
||||||
|
def __init__(self, rejections: int):
|
||||||
|
super().__init__()
|
||||||
|
self.rejections_left = rejections
|
||||||
|
self.rejected = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Count(self):
|
||||||
|
if self.rejections_left > 0:
|
||||||
|
self.rejections_left -= 1
|
||||||
|
self.rejected += 1
|
||||||
|
pywintypes = pytest.importorskip("pywintypes")
|
||||||
|
raise pywintypes.com_error(-2147418111, "Call was rejected by callee.", None, None)
|
||||||
|
return super().Count
|
||||||
|
|
||||||
|
|
||||||
|
async def test_com_ensure_document_state_waits_out_a_rejected_call(com_backend, monkeypatch):
|
||||||
|
backend, app = com_backend
|
||||||
|
busy = _BusyDocuments(rejections=3)
|
||||||
|
busy._docs.extend(app.Documents._docs)
|
||||||
|
app.Documents = busy
|
||||||
|
monkeypatch.setattr(backend, "_REJECTED_RETRY_FIRST_PAUSE_S", 0.001)
|
||||||
|
await backend._ensure_document_state()
|
||||||
|
assert busy.rejected == 3
|
||||||
|
assert backend._document_scope_key == ("First.dwg", "C:/work/First.dwg")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_com_ensure_document_state_gives_up_after_its_budget(com_backend, monkeypatch):
|
||||||
|
"""A seat that keeps rejecting (an operator mid-command) still surfaces
|
||||||
|
the error — the retry is a window, not a wait forever."""
|
||||||
|
backend, app = com_backend
|
||||||
|
busy = _BusyDocuments(rejections=10_000)
|
||||||
|
busy._docs.extend(app.Documents._docs)
|
||||||
|
app.Documents = busy
|
||||||
|
monkeypatch.setattr(backend, "_REJECTED_RETRY_FIRST_PAUSE_S", 0.001)
|
||||||
|
monkeypatch.setattr(backend, "_REJECTED_RETRY_BUDGET_S", 0.02)
|
||||||
|
pywintypes = pytest.importorskip("pywintypes")
|
||||||
|
with pytest.raises(pywintypes.com_error) as excinfo:
|
||||||
|
await backend._ensure_document_state()
|
||||||
|
assert excinfo.value.args[0] == -2147418111
|
||||||
|
assert 1 < busy.rejected < 100, "bounded by the budget, not by the rejection count"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_com_ensure_document_state_does_not_retry_other_errors(com_backend):
|
||||||
|
backend, app = com_backend
|
||||||
|
pywintypes = pytest.importorskip("pywintypes")
|
||||||
|
|
||||||
|
class _Broken(_FakeDocuments):
|
||||||
|
calls = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Count(self):
|
||||||
|
_Broken.calls += 1
|
||||||
|
raise pywintypes.com_error(-2147417848, "disconnected", None, None)
|
||||||
|
|
||||||
|
app.Documents = _Broken()
|
||||||
|
with pytest.raises(pywintypes.com_error):
|
||||||
|
await backend._ensure_document_state()
|
||||||
|
assert _Broken.calls == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_com_document_close_waits_out_the_rejection_after_close(com_backend, monkeypatch):
|
||||||
|
"""The third measured site: ``Documents.Count`` straight after
|
||||||
|
``doc.Close(False)`` inside ``document_close`` itself, before the
|
||||||
|
``_ensure_document_state`` read is even reached."""
|
||||||
|
backend, app = com_backend
|
||||||
|
busy = _BusyDocuments(rejections=0)
|
||||||
|
busy._docs.extend(app.Documents._docs)
|
||||||
|
app.Documents = busy
|
||||||
|
monkeypatch.setattr(backend, "_REJECTED_RETRY_FIRST_PAUSE_S", 0.001)
|
||||||
|
untitled = busy._docs[2]
|
||||||
|
original_close = untitled.Close
|
||||||
|
|
||||||
|
def _close_then_go_busy(save_changes):
|
||||||
|
original_close(save_changes)
|
||||||
|
busy.rejections_left = 2
|
||||||
|
|
||||||
|
untitled.Close = _close_then_go_busy
|
||||||
|
result = await backend.document_close("Drawing1.dwg", discard=True)
|
||||||
|
assert busy.rejected == 2
|
||||||
|
assert result["open_documents"] == 2 and result["active"] == "Second.dwg"
|
||||||
|
|||||||
+54
-10
@@ -290,6 +290,22 @@ class _FakeXRecord:
|
|||||||
self.deleted = True
|
self.deleted = True
|
||||||
|
|
||||||
|
|
||||||
|
class _Narrowed:
|
||||||
|
"""What pywin32 hands back for ``IAcadDictionaries.Item`` and
|
||||||
|
``IAcadDictionary.Item`` once a makepy cache exists: the gen_py
|
||||||
|
``IAcadObject`` wrapper of the *declared* return type — no ``Count``, no
|
||||||
|
``GetXRecordData`` — with the live object reachable only through
|
||||||
|
``_oleobj_`` (measured 2026-09-22, AutoCAD 2026, ``layer_state_restore``
|
||||||
|
in ``scripts/smoke_settings_com.py``: ``AttributeError: 'IAcadObject'
|
||||||
|
object has no attribute 'Count'``)."""
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
self._oleobj_ = obj
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
raise AttributeError(f"'IAcadObject' object has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
class _FakeDict:
|
class _FakeDict:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.Name = name
|
self.Name = name
|
||||||
@@ -301,7 +317,7 @@ class _FakeDict:
|
|||||||
return len(self.entries)
|
return len(self.entries)
|
||||||
|
|
||||||
def Item(self, index):
|
def Item(self, index):
|
||||||
return self.entries[index]
|
return _Narrowed(self.entries[index])
|
||||||
|
|
||||||
def GetName(self, obj):
|
def GetName(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
@@ -328,7 +344,7 @@ class _FakeDictionaries:
|
|||||||
self.calls.append(("Item", (name,)))
|
self.calls.append(("Item", (name,)))
|
||||||
if name not in self.dicts:
|
if name not in self.dicts:
|
||||||
raise RuntimeError(f"no dictionary {name}")
|
raise RuntimeError(f"no dictionary {name}")
|
||||||
return self.dicts[name]
|
return _Narrowed(self.dicts[name])
|
||||||
|
|
||||||
def Add(self, name):
|
def Add(self, name):
|
||||||
self.calls.append(("Add", (name,)))
|
self.calls.append(("Add", (name,)))
|
||||||
@@ -337,14 +353,19 @@ class _FakeDictionaries:
|
|||||||
|
|
||||||
|
|
||||||
class _FakeLayer:
|
class _FakeLayer:
|
||||||
"""Models the two refusals measured live (AutoCAD 2026): the active layer
|
"""Models the three refusals measured live (AutoCAD 2026): the active layer
|
||||||
cannot be frozen, and a frozen layer cannot be made active."""
|
cannot be frozen, a frozen layer cannot be made active, and layer ``0``
|
||||||
|
refuses the ``Freeze`` put with 'Invalid layer' whatever the value — even
|
||||||
|
``False`` on the already-thawed layer (2026-09-22, scratch document:
|
||||||
|
``Freeze=False`` on ``0`` → 'Invalid layer'; ``color`` / ``Linetype`` /
|
||||||
|
``Lineweight`` / ``Plottable`` / ``LayerOn`` / ``Lock`` / ``ActiveLayer``
|
||||||
|
all fine on it)."""
|
||||||
|
|
||||||
def __init__(self, name, **props):
|
def __init__(self, name, **props):
|
||||||
self.Name = name
|
self.Name = name
|
||||||
self.Color = props.get("color", 7)
|
self.color = props.get("color", 7)
|
||||||
self.Linetype = props.get("linetype", "Continuous")
|
self.Linetype = props.get("linetype", "Continuous")
|
||||||
self.LineWeight = props.get("lineweight", -3)
|
self.Lineweight = props.get("lineweight", -3)
|
||||||
self.LayerOn = props.get("on", True)
|
self.LayerOn = props.get("on", True)
|
||||||
self._frozen = props.get("frozen", False)
|
self._frozen = props.get("frozen", False)
|
||||||
self.Lock = props.get("locked", False)
|
self.Lock = props.get("locked", False)
|
||||||
@@ -357,6 +378,8 @@ class _FakeLayer:
|
|||||||
|
|
||||||
@Freeze.setter
|
@Freeze.setter
|
||||||
def Freeze(self, value):
|
def Freeze(self, value):
|
||||||
|
if self.Name == "0":
|
||||||
|
raise RuntimeError("AutoCAD COM error: Invalid layer")
|
||||||
if value and self.document is not None and self.document.ActiveLayer is self:
|
if value and self.document is not None and self.document.ActiveLayer is self:
|
||||||
raise RuntimeError("AutoCAD COM error: cannot freeze the current layer")
|
raise RuntimeError("AutoCAD COM error: cannot freeze the current layer")
|
||||||
self._frozen = bool(value)
|
self._frozen = bool(value)
|
||||||
@@ -447,13 +470,13 @@ async def test_com_restore_reads_the_xrecord_and_writes_layer_properties(com_bac
|
|||||||
await backend.layer_state_save("S")
|
await backend.layer_state_save("S")
|
||||||
hidden = document.Layers.Item("HIDDEN")
|
hidden = document.Layers.Item("HIDDEN")
|
||||||
hidden.Freeze = False
|
hidden.Freeze = False
|
||||||
hidden.Color = 3
|
hidden.color = 3
|
||||||
hidden.Plottable = True
|
hidden.Plottable = True
|
||||||
document.ActiveLayer = document.Layers.Item("0")
|
document.ActiveLayer = document.Layers.Item("0")
|
||||||
result = await backend.layer_state_restore("S")
|
result = await backend.layer_state_restore("S")
|
||||||
assert result["applied"]["layers"] == 3
|
assert result["applied"]["layers"] == 3
|
||||||
assert result["applied"]["current_layer"] == "GEOMETRY"
|
assert result["applied"]["current_layer"] == "GEOMETRY"
|
||||||
assert hidden.Freeze is True and hidden.Color == 8 and hidden.Plottable is False
|
assert hidden.Freeze is True and hidden.color == 8 and hidden.Plottable is False
|
||||||
assert document.ActiveLayer.Name == "GEOMETRY"
|
assert document.ActiveLayer.Name == "GEOMETRY"
|
||||||
assert result["missing_layers"] == [] and result["new_layers"] == []
|
assert result["missing_layers"] == [] and result["new_layers"] == []
|
||||||
|
|
||||||
@@ -488,17 +511,38 @@ async def test_com_restore_reports_a_refused_current_layer_instead_of_failing(co
|
|||||||
await backend.layer_state_save("PLOT")
|
await backend.layer_state_save("PLOT")
|
||||||
geometry = document.Layers.Item("GEOMETRY")
|
geometry = document.Layers.Item("GEOMETRY")
|
||||||
hidden = document.Layers.Item("HIDDEN")
|
hidden = document.Layers.Item("HIDDEN")
|
||||||
hidden.Color = 3
|
hidden.color = 3
|
||||||
document.ActiveLayer = document.Layers.Item("0")
|
document.ActiveLayer = document.Layers.Item("0")
|
||||||
geometry.Freeze = True
|
geometry.Freeze = True
|
||||||
result = await backend.layer_state_restore("PLOT", properties=["color", "current"])
|
result = await backend.layer_state_restore("PLOT", properties=["color", "current"])
|
||||||
assert result["applied"]["current_layer"] is None
|
assert result["applied"]["current_layer"] is None
|
||||||
assert result["applied"]["layers"] == 3 and hidden.Color == 8
|
assert result["applied"]["layers"] == 3 and hidden.color == 8
|
||||||
assert geometry.Freeze is True and document.ActiveLayer.Name == "0"
|
assert geometry.Freeze is True and document.ActiveLayer.Name == "0"
|
||||||
(warning,) = result["warnings"]
|
(warning,) = result["warnings"]
|
||||||
assert warning.startswith("GEOMETRY: cannot be made current: ")
|
assert warning.startswith("GEOMETRY: cannot be made current: ")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_com_restore_never_writes_freeze_on_layer_zero(com_backend):
|
||||||
|
"""Measured live (see ``_FakeLayer``): the ``Freeze`` put on layer ``0``
|
||||||
|
raises 'Invalid layer' even for ``False``, so a state saved with ``0``
|
||||||
|
current used to come back with two warnings and ``0`` unapplied. A
|
||||||
|
``Freeze`` write only happens when it would change the layer."""
|
||||||
|
backend, document = com_backend
|
||||||
|
document.ActiveLayer = document.Layers.Item("0")
|
||||||
|
await backend.layer_state_save("ZERO")
|
||||||
|
hidden = document.Layers.Item("HIDDEN")
|
||||||
|
hidden.color = 3
|
||||||
|
document.ActiveLayer = document.Layers.Item("GEOMETRY")
|
||||||
|
result = await backend.layer_state_restore("ZERO")
|
||||||
|
assert "warnings" not in result, result
|
||||||
|
assert result["applied"] == {
|
||||||
|
"layers": 3,
|
||||||
|
"properties": list(PROPERTIES),
|
||||||
|
"current_layer": "0",
|
||||||
|
}
|
||||||
|
assert document.ActiveLayer.Name == "0" and hidden.color == 8
|
||||||
|
|
||||||
|
|
||||||
async def test_com_list_and_delete(com_backend):
|
async def test_com_list_and_delete(com_backend):
|
||||||
backend, document = com_backend
|
backend, document = com_backend
|
||||||
await backend.layer_state_save("A", description="one")
|
await backend.layer_state_save("A", description="one")
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ def _fake_lwpolyline(coords, normal):
|
|||||||
Elevation=0.0,
|
Elevation=0.0,
|
||||||
Handle="2F",
|
Handle="2F",
|
||||||
Layer="0",
|
Layer="0",
|
||||||
Color=256,
|
color=256,
|
||||||
Linetype="ByLayer",
|
Linetype="ByLayer",
|
||||||
Visible=True,
|
Visible=True,
|
||||||
Closed=False,
|
Closed=False,
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ def test_com_polyline_bulges_follow_points_into_wcs():
|
|||||||
Elevation=0.0,
|
Elevation=0.0,
|
||||||
Handle="2F",
|
Handle="2F",
|
||||||
Layer="PROCESS-PIPING-MAIN",
|
Layer="PROCESS-PIPING-MAIN",
|
||||||
Color=256,
|
color=256,
|
||||||
Linetype="ByLayer",
|
Linetype="ByLayer",
|
||||||
Visible=True,
|
Visible=True,
|
||||||
Closed=False,
|
Closed=False,
|
||||||
@@ -620,7 +620,7 @@ def test_com_polyline_bulges_are_read_only_when_length_says_an_arc_exists():
|
|||||||
Elevation=0.0,
|
Elevation=0.0,
|
||||||
Handle="2F",
|
Handle="2F",
|
||||||
Layer="0",
|
Layer="0",
|
||||||
Color=256,
|
color=256,
|
||||||
Linetype="ByLayer",
|
Linetype="ByLayer",
|
||||||
Visible=True,
|
Visible=True,
|
||||||
Closed=False,
|
Closed=False,
|
||||||
@@ -885,7 +885,7 @@ def test_com_block_reference_reports_mirrored_from_its_normal():
|
|||||||
Rotation=0.0,
|
Rotation=0.0,
|
||||||
Handle="B1",
|
Handle="B1",
|
||||||
Layer="0",
|
Layer="0",
|
||||||
Color=256,
|
color=256,
|
||||||
Linetype="ByLayer",
|
Linetype="ByLayer",
|
||||||
Visible=True,
|
Visible=True,
|
||||||
Document=SimpleNamespace(Blocks=SimpleNamespace(Item=refuse)),
|
Document=SimpleNamespace(Blocks=SimpleNamespace(Item=refuse)),
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ def _com_pid_polyline(handle, coords, payload):
|
|||||||
"AcDbPolyline",
|
"AcDbPolyline",
|
||||||
Handle=handle,
|
Handle=handle,
|
||||||
Layer="PROCESS-PIPING-MAIN",
|
Layer="PROCESS-PIPING-MAIN",
|
||||||
Color=256,
|
color=256,
|
||||||
Linetype="ByLayer",
|
Linetype="ByLayer",
|
||||||
Visible=True,
|
Visible=True,
|
||||||
Closed=False,
|
Closed=False,
|
||||||
|
|||||||
@@ -795,7 +795,7 @@ class _NarrowedToIAcadEntity(_NarrowedToIAcadObject):
|
|||||||
|
|
||||||
IACADENTITY_MEMBERS = frozenset(
|
IACADENTITY_MEMBERS = frozenset(
|
||||||
{
|
{
|
||||||
"Color",
|
"color",
|
||||||
"EntityTransparency",
|
"EntityTransparency",
|
||||||
"Hyperlinks",
|
"Hyperlinks",
|
||||||
"Layer",
|
"Layer",
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ async def test_com_explode_carries_the_attrib_frame_and_style_onto_the_text(com)
|
|||||||
UpsideDown=False,
|
UpsideDown=False,
|
||||||
Alignment=4,
|
Alignment=4,
|
||||||
TextAlignmentPoint=(-20.0, 13.0, 0.0),
|
TextAlignmentPoint=(-20.0, 13.0, 0.0),
|
||||||
Color=3,
|
color=3,
|
||||||
)
|
)
|
||||||
ref = _FakeObject(
|
ref = _FakeObject(
|
||||||
"AcDbBlockReference",
|
"AcDbBlockReference",
|
||||||
@@ -537,7 +537,7 @@ async def test_com_explode_carries_the_attrib_frame_and_style_onto_the_text(com)
|
|||||||
"Alignment": 4,
|
"Alignment": 4,
|
||||||
"TextAlignmentPoint": (-20.0, 13.0, 0.0),
|
"TextAlignmentPoint": (-20.0, 13.0, 0.0),
|
||||||
"Layer": "PID-TAG",
|
"Layer": "PID-TAG",
|
||||||
"Color": 3,
|
"color": 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -963,7 +963,7 @@ async def test_com_explode_turns_a_multi_line_attrib_into_mtext(com):
|
|||||||
Rotation=0.5236,
|
Rotation=0.5236,
|
||||||
StyleName="ISO",
|
StyleName="ISO",
|
||||||
Layer="PID-TAG",
|
Layer="PID-TAG",
|
||||||
Color=3,
|
color=3,
|
||||||
Invisible=False,
|
Invisible=False,
|
||||||
)
|
)
|
||||||
plain = _FakeObject(
|
plain = _FakeObject(
|
||||||
@@ -1010,7 +1010,7 @@ async def test_com_explode_turns_a_multi_line_attrib_into_mtext(com):
|
|||||||
("AttachmentPoint", 9), # acAttachmentPointBottomRight
|
("AttachmentPoint", 9), # acAttachmentPointBottomRight
|
||||||
("InsertionPoint", (7.0, 15.2, 0.0)), # re-asserted after the attachment moved it
|
("InsertionPoint", (7.0, 15.2, 0.0)), # re-asserted after the attachment moved it
|
||||||
("Layer", "PID-TAG"),
|
("Layer", "PID-TAG"),
|
||||||
("Color", 3),
|
("color", 3),
|
||||||
]
|
]
|
||||||
assert tuple(space.texts[0].InsertionPoint.value) == (7.0, 15.2, 0.0), (
|
assert tuple(space.texts[0].InsertionPoint.value) == (7.0, 15.2, 0.0), (
|
||||||
"the MTEXT ends up anchored at the ATTRIB's alignment point"
|
"the MTEXT ends up anchored at the ATTRIB's alignment point"
|
||||||
|
|||||||
Reference in New Issue
Block a user