fix(task-17): COM page setup reads mm as mm, 1:5 takes the custom route, and a refused apply puts the media back

Three live-measured behaviours of AutoCAD 2026 that the fake did not replay:

* GetPaperSize / GetPaperMargins answer in millimetres whatever PaperUnits
  says; the 25.4 factor listed every imperial layout as [10668.0, 7543.8]
  and lost the paper name. No factor now; the fake pins PaperUnits=0.
* Layout.StandardScale = 19 (ac1_5) is refused with "Invalid input" in every
  condition; '1:5' is dropped from ACTIVEX_PLOT_SCALE so it goes through
  SetCustomScale(1, 5) like 5:1 already did.
* Writing ConfigName replaces CanonicalMediaName with the new device's
  default, and writing the old device back does not bring the media back —
  the unknown-media refusal and a late refusal both left the layout on the
  old device's default paper while the error claimed it was restored. The
  device write now snapshots media / units / rotation and journals one
  combined undo; the read-back after every unwind is compared to the
  read-back before the first write and the message names what did not go
  back instead of asserting it did.

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 09:42:13 +03:00
co-authored by Claude Opus 5
parent 20ca39d28a
commit 376dc812a5
3 changed files with 184 additions and 16 deletions
+56 -8
View File
@@ -1968,12 +1968,34 @@ class ComBackend(AutoCADBackend):
# over None), an extents plot writes it *after* PlotType, and a stale True
# is cleared *before* PlotType moves to acLayout. Every write is journaled
# and unwound in reverse when AutoCAD refuses one, so a failed apply leaves
# the layout as it was. Fake-tested in tests/test_page_setup.py (the fake
# enforces the CenterPlot rule); executed live by
# the layout as it was — and the read-back after the unwind is compared to
# the read-back before the first write, so a value that did not go back is
# named rather than claimed restored.
#
# Two behaviours measured live (AutoCAD 2026, 2026-09-17) that the ActiveX
# Reference does not document:
# * GetPaperSize / GetPaperMargins answer in millimetres whatever
# PaperUnits says (ISO_A3 under PaperUnits=0 -> (420.0, 297.0); ANSI_B
# under 0 and 1 -> (431.8, 279.4) both times). They are never scaled.
# * Writing ConfigName replaces CanonicalMediaName with the new device's
# default when the current media does not exist on it, and writing the
# old device back does *not* bring the old media with it (DWG To PDF /
# ISO_A3 -> Microsoft Print to PDF -> 'psk:ISOA4' -> DWG To PDF ->
# 'ANSI_A_(11.00_x_8.50_Inches)'). The device write therefore journals
# one combined undo: device back, then the media / units / rotation
# snapshotted before the switch re-applied.
#
# Fake-tested in tests/test_page_setup.py (the fake enforces the CenterPlot
# rule and replays both measurements); executed live by
# scripts/smoke_settings_com.py (Task 24).
_AC_MILLIMETERS = 1
_AC_NO_ROTATION = 0
#: Layout properties a ConfigName write can move as a side effect (measured
#: live for CanonicalMediaName; units / rotation are re-applied on the same
#: evidence-before-trust basis) — snapshotted before the device switch and
#: put back by its undo.
_DEVICE_SIDE_EFFECTS = ("CanonicalMediaName", "PaperUnits", "PlotRotation")
@staticmethod
def _com_page_setup_row(layout) -> dict:
@@ -1992,18 +2014,18 @@ class ComBackend(AutoCADBackend):
log.debug("Layout.%s read failed: %s", attr, exc)
return None
units = _get("PaperUnits")
factor = 25.4 if units == 0 else 1.0
# GetPaperSize / GetPaperMargins are millimetres regardless of
# PaperUnits (measured live; see the block comment above) — no factor.
size = None
try:
width, height = layout.GetPaperSize()
size = [round(float(width) * factor, 2), round(float(height) * factor, 2)]
size = [round(float(width), 2), round(float(height), 2)]
except Exception as exc:
log.debug("Layout.GetPaperSize failed: %s", exc)
margins = None
try:
(left, bottom), (right, top) = layout.GetPaperMargins()
margins = [round(float(v) * factor, 3) for v in (top, bottom, left, right)]
margins = [round(float(v), 3) for v in (top, bottom, left, right)]
except Exception as exc:
log.debug("Layout.GetPaperMargins failed: %s", exc)
rotation = _get("PlotRotation") or 0
@@ -2094,10 +2116,27 @@ class ComBackend(AutoCADBackend):
target.SetCustomScale(numerator, denominator)
journal.append((lambda: target.SetCustomScale(*previous), "SetCustomScale"))
def _set_device(device: str):
# A device switch moves the media (and can move units /
# rotation) as a side effect, and switching back does not
# move them back — so the undo re-applies what was there.
snapshot = {attr: getattr(target, attr) for attr in self._DEVICE_SIDE_EFFECTS}
previous = target.ConfigName
target.ConfigName = device
def _undo():
target.ConfigName = previous
target.RefreshPlotDeviceInfo()
for attr, value in snapshot.items():
if getattr(target, attr) != value:
setattr(target, attr, value)
journal.append((_undo, "ConfigName"))
step = "ConfigName"
try:
target.RefreshPlotDeviceInfo()
_set("ConfigName", resolved["device"])
_set_device(resolved["device"])
target.RefreshPlotDeviceInfo()
media = resolved["canonical_media_name"]
names = [str(n) for n in (target.GetCanonicalMediaNames() or ())]
@@ -2144,14 +2183,23 @@ class ComBackend(AutoCADBackend):
_set("CenterPlot", bool(center))
except Exception as exc:
failed = self._com_unwind_page_setup(journal)
# The unwind's own word is not evidence: read the layout back
# and name every field that is not what it was.
failed += [
key
for key, value in self._com_page_setup_row(target).items()
if key != "layout" and value != before.get(key) and key not in failed
]
if isinstance(exc, ValueError) and not failed:
raise
raise # our own refusal (unknown media), nothing left changed
restored = (
f"layout {target.Name!r} restored to its previous page setup"
if not failed
else f"layout {target.Name!r} could NOT be fully restored "
f"(still changed: {', '.join(failed)})"
)
if isinstance(exc, ValueError):
raise ValueError(f"{exc}; {restored}") from exc
raise ValueError(
f"page_setup_apply: AutoCAD refused Layout.{step} ({exc}); {restored}"
) from exc