diff --git a/backends/com_backend.py b/backends/com_backend.py index 227dea0..17809bf 100644 --- a/backends/com_backend.py +++ b/backends/com_backend.py @@ -13,6 +13,7 @@ import math import sys import time import uuid +from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor from pathlib import Path from typing import Any @@ -1961,7 +1962,14 @@ class ComBackend(AutoCADBackend): # typelib: AcPlotPaperUnits acMillimeters=1; AcPlotRotation ac0degrees=0; # AcPlotType acExtents=1 / acLayout=5; AcPlotScale acScaleToFit=0, ac1_1=16 # (the full table is engineering.standards.papers.ACTIVEX_PLOT_SCALE). - # Fake-tested in tests/test_page_setup.py; executed live by + # CenterPlot (ActiveX Reference, acadauto.chm): "This property cannot be + # set to True on a layout object whose PlotType property is set to + # acLayout" — so a layout plot never writes it (resolve_page_setup hands + # 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 # scripts/smoke_settings_com.py (Task 24). _AC_MILLIMETERS = 1 @@ -1972,6 +1980,7 @@ class ComBackend(AutoCADBackend): from engineering.standards.papers import ( PLOT_TYPE_NAMES, activex_scale_label, + center_applies, paper_from_size, scale_label, ) @@ -2024,7 +2033,7 @@ class ComBackend(AutoCADBackend): "plot_area": PLOT_TYPE_NAMES.get(plot_type, plot_type), "device": _get("ConfigName"), "margins_mm": margins, - "center": bool(_get("CenterPlot")), + "center": bool(_get("CenterPlot")) if center_applies(plot_type) else None, } def _com_paper_layout(self, doc, raw: str, operation: str): @@ -2071,37 +2080,81 @@ class ComBackend(AutoCADBackend): doc = _acad_doc() target = self._com_paper_layout(doc, layout, "page_setup_apply") before = self._com_page_setup_row(target) - previous_device = before["device"] - target.RefreshPlotDeviceInfo() - target.ConfigName = resolved["device"] - target.RefreshPlotDeviceInfo() - media = resolved["canonical_media_name"] - names = [str(n) for n in (target.GetCanonicalMediaNames() or ())] - if media not in names: - # Roll the one write back before refusing, so a refusal leaves - # the layout as it was. - target.ConfigName = previous_device - nearest = [n for n in names if n.split("_(")[0] == media.split("_(")[0]] - raise ValueError( - f"page_setup_apply: device {resolved['device']!r} has no media " - f"{media!r} (same paper on this device: {', '.join(nearest) or 'none'}; " - f"{len(names)} media in total)" - ) - target.CanonicalMediaName = media - target.PaperUnits = self._AC_MILLIMETERS - target.PlotRotation = self._AC_NO_ROTATION - target.StyleSheet = resolved["plot_style"] - target.PlotWithPlotStyles = True - target.PlotType = int(resolved["plot_type"]) - code = resolved["activex_standard_scale"] - if code is not None: - target.StandardScale = int(code) - target.UseStandardScale = True - else: - numerator, denominator = (float(v) for v in resolved["scale_ratio"]) + # Journal of (undo callable, label) for every write that landed, + # unwound in reverse when a later one is refused. + journal: list[tuple[Callable[[], None], str]] = [] + + def _set(attr: str, value): + previous = getattr(target, attr) + setattr(target, attr, value) + journal.append((lambda: setattr(target, attr, previous), attr)) + + def _set_custom_scale(numerator: float, denominator: float): + previous = tuple(float(v) for v in target.GetCustomScale()) target.SetCustomScale(numerator, denominator) - target.UseStandardScale = False - target.CenterPlot = bool(resolved["center"]) + journal.append((lambda: target.SetCustomScale(*previous), "SetCustomScale")) + + step = "ConfigName" + try: + target.RefreshPlotDeviceInfo() + _set("ConfigName", resolved["device"]) + target.RefreshPlotDeviceInfo() + media = resolved["canonical_media_name"] + names = [str(n) for n in (target.GetCanonicalMediaNames() or ())] + if media not in names: + nearest = [n for n in names if n.split("_(")[0] == media.split("_(")[0]] + raise ValueError( + f"page_setup_apply: device {resolved['device']!r} has no media " + f"{media!r} (same paper on this device: " + f"{', '.join(nearest) or 'none'}; {len(names)} media in total)" + ) + step = "CanonicalMediaName" + _set("CanonicalMediaName", media) + step = "PaperUnits" + _set("PaperUnits", self._AC_MILLIMETERS) + step = "PlotRotation" + _set("PlotRotation", self._AC_NO_ROTATION) + step = "StyleSheet" + _set("StyleSheet", resolved["plot_style"]) + step = "PlotWithPlotStyles" + _set("PlotWithPlotStyles", True) + center = resolved["center"] + if center is None and self._com_center_plot_is_set(target): + # Clearing is legal under any PlotType; the layout plot + # then carries the flag AutoCAD stores for acLayout. + step = "CenterPlot" + _set("CenterPlot", False) + step = "PlotType" + _set("PlotType", int(resolved["plot_type"])) + code = resolved["activex_standard_scale"] + if code is not None: + step = "StandardScale" + _set("StandardScale", int(code)) + step = "UseStandardScale" + _set("UseStandardScale", True) + else: + numerator, denominator = (float(v) for v in resolved["scale_ratio"]) + step = "SetCustomScale" + _set_custom_scale(numerator, denominator) + step = "UseStandardScale" + _set("UseStandardScale", False) + if center is not None: + # Extents plot: PlotType is already acExtents, so True is legal. + step = "CenterPlot" + _set("CenterPlot", bool(center)) + except Exception as exc: + failed = self._com_unwind_page_setup(journal) + if isinstance(exc, ValueError) and not failed: + raise + 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)})" + ) + raise ValueError( + f"page_setup_apply: AutoCAD refused Layout.{step} ({exc}); {restored}" + ) from exc after = self._com_page_setup_row(target) changed = { key: [before[key], after[key]] @@ -2120,6 +2173,27 @@ class ComBackend(AutoCADBackend): return await self._run(_sync) + @staticmethod + def _com_center_plot_is_set(layout) -> bool: + """Whether the layout carries CenterPlot=True (a failed read counts as no).""" + try: + return bool(layout.CenterPlot) + except Exception as exc: + log.debug("Layout.CenterPlot read failed: %s", exc) + return False + + @staticmethod + def _com_unwind_page_setup(journal: list[tuple[Callable[[], None], str]]) -> list[str]: + """Undo every journaled page-setup write in reverse; return what would not go back.""" + failed: list[str] = [] + for undo, label in reversed(journal): + try: + undo() + except Exception as exc: + log.warning("page_setup_apply: could not restore Layout.%s: %s", label, exc) + failed.append(label) + return failed + async def plot_style_list(self) -> list[dict]: # Completed in Task 18 (installed ctb scan); the catalogue rows already answer. from engineering.standards.papers import CTB_CATALOG diff --git a/backends/contracts/pagesetup.py b/backends/contracts/pagesetup.py index 464e00e..9226d8f 100644 --- a/backends/contracts/pagesetup.py +++ b/backends/contracts/pagesetup.py @@ -18,7 +18,10 @@ class PageSetupContract(ABC): orientation, plot_style, scale, plot_area, device, margins_mm: [top, bottom, left, right], center}``. ``paper`` is the catalogue name when the size matches one within 0.5 mm, otherwise the - media name as stored. Raises ``ValueError`` for an unknown layout. + media name as stored. ``center`` is a bool for an extents plot and + ``None`` for a layout plot, where centring is not applicable (AutoCAD + greys it out; ActiveX refuses ``CenterPlot = True`` under + ``acLayout``). Raises ``ValueError`` for an unknown layout. ``Model`` is never listed: model space is plotted through ``drawing_export_pdf(layout=None)`` and carries no sheet. """ @@ -33,7 +36,9 @@ class PageSetupContract(ABC): plot_style_known, viewports_kept}``. ``changed`` is computed from the read-back before and after, so re-applying the same setup reports ``{}``. Refuses ``Model``, an unknown layout, and a ``setup`` missing a - resolved key, before anything is written. Never deletes viewports. + resolved key, before anything is written. A write the engine refuses + part-way through is unwound (every landed value put back) before the + ``ValueError`` names the refused property. Never deletes viewports. """ ... diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index 717512f..61f01e9 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -2336,6 +2336,7 @@ class EzdxfBackend(AutoCADBackend): def _page_setup_row(layout) -> dict: from engineering.standards.papers import ( PLOT_TYPE_NAMES, + center_applies, dxf_scale_label, paper_from_size, scale_label, @@ -2355,6 +2356,7 @@ class EzdxfBackend(AutoCADBackend): scale = dxf_scale_label(int(dxf.standard_scale_type), numerator, denominator) else: scale = scale_label(numerator, denominator) + plot_type = int(dxf.plot_type) return { "layout": layout.name, "paper": paper_from_size(width, height) or media, @@ -2363,7 +2365,7 @@ class EzdxfBackend(AutoCADBackend): "orientation": "landscape" if width >= height else "portrait", "plot_style": str(dxf.current_style_sheet), "scale": scale, - "plot_area": PLOT_TYPE_NAMES.get(int(dxf.plot_type), str(dxf.plot_type)), + "plot_area": PLOT_TYPE_NAMES.get(plot_type, str(dxf.plot_type)), "device": str(dxf.plot_configuration_file), "margins_mm": [ round(float(dxf.top_margin), 3), @@ -2371,7 +2373,9 @@ class EzdxfBackend(AutoCADBackend): round(float(dxf.left_margin), 3), round(float(dxf.right_margin), 3), ], - "center": bool(flags & layout.PLOT_CENTERED), + # A layout plot has no centring (AutoCAD greys it out; ActiveX + # refuses CenterPlot=True under acLayout): None, not a bit nobody honours. + "center": bool(flags & layout.PLOT_CENTERED) if center_applies(plot_type) else None, } @staticmethod @@ -2474,6 +2478,9 @@ class EzdxfBackend(AutoCADBackend): dxf.standard_scale_type = 16 dxf.scale_numerator, dxf.scale_denominator = numerator, denominator target.use_standard_scale(False) + # resolve_page_setup hands over None for a layout plot: the bit is + # cleared, never set — the state AutoCAD itself stores for acLayout, + # and the mirror of the live engine never writing CenterPlot there. target.plot_centered(bool(resolved["center"])) # Limits follow the paper; viewports are deliberately left alone # (Paperspace.page_setup() would delete them). diff --git a/engineering/standards/papers.py b/engineering/standards/papers.py index 54e7248..0f97c6c 100644 --- a/engineering/standards/papers.py +++ b/engineering/standards/papers.py @@ -13,6 +13,16 @@ tables of two enums that do not agree with each other: A scale without a code on an engine goes through the custom numerator / denominator on that engine, and the read-back reports the same label either way. + +Centring belongs to an *extents* plot only. The ActiveX Reference (CenterPlot +Property, acadauto.chm, AutoCAD 2026): "This property cannot be set to True on +a layout object whose PlotType property is set to acLayout" — a layout plot is +the whole sheet from its own origin, and the Page Setup dialog greys the box +out. ``resolve_page_setup`` therefore resolves ``center`` to ``None`` (not +applicable, never written, read back as ``None``) when ``plot_area`` is +``layout``; the bool is kept only for ``extents``. Both engines follow the +same resolved value, so the DXF never carries a centre bit AutoCAD would +ignore and the live engine never issues the write AutoCAD refuses. """ from __future__ import annotations @@ -28,11 +38,13 @@ __all__ = [ "PAPER_INCHES", "PAPER_SIZES", "PLOT_AREAS", + "PLOT_TYPE_LAYOUT", "PLOT_TYPE_NAMES", "REQUIRED_SETUP_KEYS", "SCALES", "activex_scale_label", "canonical_media_name", + "center_applies", "dxf_scale_label", "paper_from_size", "paper_size_mm", @@ -135,6 +147,8 @@ _ACTIVEX_SCALE_LABEL = {code: label for label, code in ACTIVEX_PLOT_SCALE.items( #: Plot area → DXF/ActiveX plot type (``AcPlotType``: acExtents=1, acLayout=5). PLOT_AREAS: dict[str, int] = {"layout": 5, "extents": 1} +#: ``AcPlotType.acLayout`` — the plot type under which centring does not apply. +PLOT_TYPE_LAYOUT = 5 PLOT_TYPE_NAMES: dict[int, str] = { 0: "display", 1: "extents", @@ -243,6 +257,19 @@ def plot_style_known(name) -> bool: return str(name or "").strip().lower() in {entry.lower() for entry in CTB_CATALOG} +def center_applies(plot_type) -> bool: + """Whether "centre the plot" means anything under this ``AcPlotType``. + + False for ``acLayout`` (5): the sheet is plotted from its origin, AutoCAD + greys the option out and ActiveX refuses ``CenterPlot = True``. Read-backs + report ``center: None`` there instead of a stored flag nothing honours. + """ + try: + return int(plot_type) != PLOT_TYPE_LAYOUT + except (TypeError, ValueError): + return False + + def resolve_page_setup( paper: str, orientation: str = "landscape", @@ -256,7 +283,10 @@ def resolve_page_setup( """Validate a page setup request and resolve it to what both engines write. Raises ``ValueError`` naming the offending field; nothing here touches a - drawing. ``margins_mm`` is ``[top, bottom, left, right]``. + drawing. ``margins_mm`` is ``[top, bottom, left, right]``. ``center`` is + validated as a bool and resolved to ``None`` for ``plot_area="layout"``, + where centring is not applicable (see the module docstring) — the default + call (layout, centre) is therefore a layout plot with no centring write. """ name = _paper_key(paper) width, height = paper_size_mm(name, _orientation(orientation)) @@ -300,6 +330,7 @@ def resolve_page_setup( if not isinstance(center, bool): raise ValueError(f"center: must be true or false, got {center!r}") + plot_type = PLOT_AREAS[plot_area] return { "paper": name, @@ -314,10 +345,10 @@ def resolve_page_setup( "dxf_standard_scale_type": DXF_STANDARD_SCALE_TYPE.get(label), "activex_standard_scale": ACTIVEX_PLOT_SCALE.get(label), "plot_area": plot_area, - "plot_type": PLOT_AREAS[plot_area], + "plot_type": plot_type, "device": device_name, "margins_mm": margins, - "center": center, + "center": center if center_applies(plot_type) else None, } diff --git a/tests/test_page_setup.py b/tests/test_page_setup.py index afd0604..1e93940 100644 --- a/tests/test_page_setup.py +++ b/tests/test_page_setup.py @@ -83,7 +83,21 @@ def test_resolve_page_setup_defaults(): assert setup["scale_ratio"] == [1.0, 1.0] assert setup["plot_area"] == "layout" and setup["plot_type"] == 5 assert setup["device"] == "DWG To PDF.pc3" - assert setup["margins_mm"] is None and setup["center"] is True + assert setup["margins_mm"] is None + assert setup["center"] is None, "a layout plot has no centring (AutoCAD greys it out)" + + +def test_resolve_page_setup_center_applies_to_an_extents_plot_only(): + """ActiveX Reference, CenterPlot: 'This property cannot be set to True on a + layout object whose PlotType property is set to acLayout.' The resolver + carries the rule so both engines write the same thing: a bool for an + extents plot, ``None`` (not applicable, never written) for a layout plot.""" + assert resolve_page_setup("ISO_A3", plot_area="extents")["center"] is True + assert resolve_page_setup("ISO_A3", plot_area="extents", center=False)["center"] is False + assert resolve_page_setup("ISO_A3", plot_area="layout", center=True)["center"] is None + assert resolve_page_setup("ISO_A3", plot_area="layout", center=False)["center"] is None + with pytest.raises(ValueError, match="center"): + resolve_page_setup("ISO_A3", plot_area="layout", center="yes") def test_resolve_page_setup_accepts_lowercase_and_short_paper_names(): @@ -196,7 +210,7 @@ async def test_apply_iso_a3_landscape_and_the_pdf_mediabox_agrees(backend, tmp_p assert row["scale"] == "fit" assert row["plot_area"] == "layout" assert row["device"] == "DWG To PDF.pc3" - assert row["center"] is True + assert row["center"] is None, "layout plot: centring is not applicable" assert len(row["margins_mm"]) == 4 width, height = await _mediabox_after_export(backend, tmp_path, SHEET) @@ -282,16 +296,45 @@ async def test_apply_materialises_the_default_flags_before_setting_centre(backen """Measured: set_flag_state starts from 0 on a fresh layout, so a naive plot_centered(True) writes 4 and drops lineweights/plot-styles/viewports-first.""" await _sheet(backend) - await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", center=True)) + extents = resolve_page_setup("ISO_A3", plot_area="extents", center=True) + result = await backend.page_setup_apply(SHEET, extents) flags = int(backend._doc.layouts.get(SHEET).dxf_layout.dxf.plot_layout_flags) assert flags & 4, "centre-plot bit" assert flags & 32, "plot with plot styles" assert flags & 128, "plot entity lineweights" assert flags & 512, "draw viewports first" assert flags & 16, "use standard scale (fit)" - await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", center=False)) + assert result["applied"]["center"] is True + assert (await backend.page_setup_list(SHEET))[0]["center"] is True + await backend.page_setup_apply( + SHEET, resolve_page_setup("ISO_A3", plot_area="extents", center=False) + ) flags = int(backend._doc.layouts.get(SHEET).dxf_layout.dxf.plot_layout_flags) assert not flags & 4 and flags & 32 and flags & 128 and flags & 512 + assert (await backend.page_setup_list(SHEET))[0]["center"] is False + + +async def test_layout_plot_never_sets_the_centre_bit_and_reports_it_not_applicable(backend): + """A layout plot is the whole sheet from its origin: AutoCAD disables + 'Center the plot' (ActiveX refuses CenterPlot=True under acLayout). The + headless engine mirrors that: bit 4 is not written, and the read-back says + None rather than a bool AutoCAD would ignore.""" + await _sheet(backend) + result = await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", center=True)) + flags = int(backend._doc.layouts.get(SHEET).dxf_layout.dxf.plot_layout_flags) + assert not flags & 4, "centre bit must not be set for a layout plot" + assert result["applied"]["center"] is None + assert "center" not in result["changed"], "None before (fresh layout plot) and None after" + assert (await backend.page_setup_list(SHEET))[0]["center"] is None + # Switching an extents+centred sheet to a layout plot clears the stale bit + # (what AutoCAD stores for a layout plot) and reports the move honestly. + await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", plot_area="extents")) + assert (await backend.page_setup_list(SHEET))[0]["center"] is True + result = await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3")) + flags = int(backend._doc.layouts.get(SHEET).dxf_layout.dxf.plot_layout_flags) + assert not flags & 4 + assert result["changed"]["center"] == [True, None] + assert result["changed"]["plot_area"] == ["extents", "layout"] async def test_custom_scale_clears_the_standard_scale_bit_and_writes_the_ratio(backend): @@ -370,11 +413,21 @@ _MEDIA_RE = re.compile(r"\((\d+\.\d+)_x_(\d+\.\d+)_(MM|Inches)\)") class _FakeLayout: - """Records every property set and method call, in order.""" + """Records every property set and method call, in order. - def __init__(self, name, media_names): + Enforces the one rule the ActiveX Reference documents for this surface + (CenterPlot Property, acadauto.chm, AutoCAD 2026): "This property cannot + be set to True on a layout object whose PlotType property is set to + acLayout." ``refuse`` names properties whose write raises, the way a live + AutoCAD refuses a value it cannot take. + """ + + AC_LAYOUT = 5 + + def __init__(self, name, media_names, refuse=()): object.__setattr__(self, "log", []) object.__setattr__(self, "_media", tuple(media_names)) + object.__setattr__(self, "_refuse", tuple(refuse)) object.__setattr__( self, "_props", @@ -400,9 +453,16 @@ class _FakeLayout: raise AttributeError(attr) def __setattr__(self, attr, value): + if attr in self._refuse: + raise RuntimeError(f"AutoCAD refused Layout.{attr} = {value!r}") + if attr == "CenterPlot" and value and self._props["PlotType"] == self.AC_LAYOUT: + raise RuntimeError("CenterPlot cannot be True while PlotType is acLayout") self._props[attr] = value self.log.append(("set", attr, value)) + def snapshot(self) -> dict: + return dict(object.__getattribute__(self, "_props")) + def RefreshPlotDeviceInfo(self): self.log.append(("call", "RefreshPlotDeviceInfo")) @@ -487,11 +547,67 @@ async def test_com_apply_issues_the_property_sequence(com_backend): ("set", "PlotType", 5), ("set", "StandardScale", 0), ("set", "UseStandardScale", True), - ("set", "CenterPlot", True), - ] + ], "a layout plot never writes CenterPlot (ActiveX refuses True under acLayout)" assert result["changed"]["paper"] == ["ISO_A4", "ISO_A3"] assert result["changed"]["device"] == ["None", "DWG To PDF.pc3"] - assert result["changed"]["center"] == [False, True] + assert result["applied"]["center"] is None + assert "center" not in result["changed"] + + +async def test_com_apply_extents_sets_plot_type_before_center_plot(com_backend): + backend, document = com_backend + result = await backend.page_setup_apply( + "Layout1", resolve_page_setup("ISO_A3", plot_area="extents", center=True) + ) + log = document.Layouts.Item("Layout1").log + assert log.index(("set", "PlotType", 1)) < log.index(("set", "CenterPlot", True)) + assert result["changed"]["center"] == [None, True] + assert result["changed"]["plot_area"] == ["layout", "extents"] + assert (await backend.page_setup_list("Layout1"))[0]["center"] is True + + +async def test_com_apply_clears_a_stale_center_plot_before_switching_to_layout(com_backend): + backend, document = com_backend + layout = document.Layouts.Item("Layout1") + layout.PlotType = 1 + layout.CenterPlot = True + del layout.log[:] + result = await backend.page_setup_apply("Layout1", resolve_page_setup("ISO_A3")) + log = layout.log + assert log.index(("set", "CenterPlot", False)) < log.index(("set", "PlotType", 5)) + assert layout.CenterPlot is False and layout.PlotType == 5 + assert result["changed"]["center"] == [True, None] + + +async def test_com_apply_restores_every_write_when_autocad_refuses_a_property(com_backend): + """Nine writes had landed before CenterPlot in the shipped sequence, with + nothing put back. Any refusal now unwinds the journal in reverse.""" + backend, document = com_backend + layout = _FakeLayout("Layout1", MEDIA, refuse=("PlotWithPlotStyles",)) + document.Layouts.layouts[1] = layout + before = layout.snapshot() + with pytest.raises(ValueError, match="PlotWithPlotStyles") as info: + await backend.page_setup_apply("Layout1", resolve_page_setup("ISO_A3")) + assert "restored" in str(info.value) + assert layout.snapshot() == before, "every property AutoCAD accepted is put back" + restores = [entry for entry in layout.log if entry[0] == "set"][5:] + assert restores == [ + ("set", "StyleSheet", ""), + ("set", "PlotRotation", 0), + ("set", "PaperUnits", 1), + ("set", "CanonicalMediaName", "ISO_A4_(210.00_x_297.00_MM)"), + ("set", "ConfigName", "None"), + ], "unwound in reverse write order" + + +async def test_com_apply_restores_the_custom_scale_too(com_backend): + backend, document = com_backend + layout = _FakeLayout("Layout1", MEDIA, refuse=("UseStandardScale",)) + document.Layouts.layouts[1] = layout + with pytest.raises(ValueError, match="UseStandardScale"): + await backend.page_setup_apply("Layout1", resolve_page_setup("ISO_A3", scale="5:1")) + calls = [entry for entry in layout.log if entry[0] == "call" and entry[1] == "SetCustomScale"] + assert calls == [("call", "SetCustomScale", 5.0, 1.0), ("call", "SetCustomScale", 1.0, 1.0)] async def test_com_apply_custom_scale_uses_set_custom_scale(com_backend): @@ -539,7 +655,17 @@ async def test_com_list_reads_the_properties(com_backend): assert row["orientation"] == "portrait" assert row["margins_mm"] == [20.0, 20.0, 7.5, 7.5] assert row["scale"] == "1:1" and row["plot_area"] == "layout" - assert row["device"] == "None" and row["center"] is False + assert row["device"] == "None" + assert row["center"] is None, "acLayout: CenterPlot is not applicable" + + +async def test_com_list_reports_center_for_an_extents_plot(com_backend): + backend, document = com_backend + layout = document.Layouts.Item("Layout1") + layout.PlotType = 1 + assert (await backend.page_setup_list("Layout1"))[0]["center"] is False + layout.CenterPlot = True + assert (await backend.page_setup_list("Layout1"))[0]["center"] is True async def test_com_declares_dwt_write():