fix(task-17): a rotated sheet's margins turn with it, and a paper-units move is named in changed and warnings

Finding 1: page_setup_list swapped size_mm for plot_rotation 1/3 but
reported the margins un-turned, so a 297 x 210 sheet carried its portrait
margins ([20, 20, 7.5, 7.5], printable 282 x 170) while ezdxf's Page and
_paper_frame plotted [7.5, 7.5, 20, 20] (257 x 195). The mapping now lives
once in papers.turned_sheet (cross-checked against Page.from_dxf_layout
for all four rotations) and is used by the ezdxf row, _paper_frame and the
COM row. page_setup_apply resets plot_rotation to 0; headlessly it now
re-writes the turned margins un-turned so the sheet keeps the margins it
had (nothing moves, changed says nothing), and on COM, where margins come
from the .pc3, the ones that moved show up in changed.

Finding 2: both engines forced plot paper units to millimetres with no
units field in the row, so an inch-authored ANSI sheet was rescaled 25.4x
with ok: True and changed naming only the media. Rows now carry
paper_units (mm | inches | pixels, DXF group 72 / AcPlotPaperUnits);
resolve_page_setup takes paper_units="mm" (default) | "inches" | None
(keep the layout's units, as AutoCAD's own Page Setup does); a move is
reported in changed and, with its 25.4x consequence and the way back, in
the new warnings list (papers.page_setup_warnings, identical 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:
Umut CAN
2026-09-17 14:58:17 +03:00
co-authored by Claude Opus 5
parent 4a6591843f
commit f2386a9fdd
5 changed files with 501 additions and 51 deletions
+27 -5
View File
@@ -1977,6 +1977,14 @@ class ComBackend(AutoCADBackend):
# * 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.
# PaperUnits itself still governs what one paper-space unit means at
# the plot scale, so the row reports it (``paper_units``) and an apply
# that moves it says so in ``changed`` and ``warnings``.
# * Both answer in the *media's* frame (the PLOTSETTINGS fields the DXF
# row reads); under PlotRotation 1/3 the row turns size and margins
# together through ``papers.turned_sheet``, the one mapping the headless
# row and renderer use. The rotated read-back is not yet measured live
# (scripts/smoke_settings_com.py, Task 24, is where it gets measured).
# * 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 /
@@ -1989,7 +1997,6 @@ class ComBackend(AutoCADBackend):
# 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
@@ -2004,7 +2011,9 @@ class ComBackend(AutoCADBackend):
activex_scale_label,
center_applies,
paper_from_size,
paper_units_name,
scale_label,
turned_sheet,
)
def _get(attr):
@@ -2029,7 +2038,11 @@ class ComBackend(AutoCADBackend):
except Exception as exc:
log.debug("Layout.GetPaperMargins failed: %s", exc)
rotation = _get("PlotRotation") or 0
if size and int(rotation) in (1, 3):
if size and margins:
size, margins = turned_sheet(size[0], size[1], margins, rotation)
size = [round(v, 2) for v in size]
margins = [round(v, 3) for v in margins]
elif size and int(rotation) in (1, 3):
size = [size[1], size[0]]
if _get("UseStandardScale"):
scale = activex_scale_label(_get("StandardScale") or 0)
@@ -2055,6 +2068,7 @@ class ComBackend(AutoCADBackend):
"plot_area": PLOT_TYPE_NAMES.get(plot_type, plot_type),
"device": _get("ConfigName"),
"margins_mm": margins,
"paper_units": paper_units_name(_get("PaperUnits")),
"center": bool(_get("CenterPlot")) if center_applies(plot_type) else None,
}
@@ -2088,7 +2102,11 @@ class ComBackend(AutoCADBackend):
return await self._run(_sync)
async def page_setup_apply(self, layout: str, setup: dict) -> dict:
from engineering.standards.papers import require_page_setup
from engineering.standards.papers import (
PAPER_UNITS,
page_setup_warnings,
require_page_setup,
)
resolved = require_page_setup(setup)
if resolved["margins_mm"] is not None:
@@ -2149,8 +2167,11 @@ class ComBackend(AutoCADBackend):
)
step = "CanonicalMediaName"
_set("CanonicalMediaName", media)
step = "PaperUnits"
_set("PaperUnits", self._AC_MILLIMETERS)
if resolved["paper_units"] is not None:
# None keeps the layout's units — what AutoCAD's own Page
# Setup does when a media is chosen.
step = "PaperUnits"
_set("PaperUnits", PAPER_UNITS[resolved["paper_units"]])
step = "PlotRotation"
_set("PlotRotation", self._AC_NO_ROTATION)
step = "StyleSheet"
@@ -2214,6 +2235,7 @@ class ComBackend(AutoCADBackend):
"layout": target.Name,
"applied": dict(resolved),
"changed": changed,
"warnings": page_setup_warnings(changed),
"plot_style_known": bool(resolved["plot_style_known"]),
"viewports_kept": True,
"backend": "com",
+26 -8
View File
@@ -16,12 +16,20 @@ class PageSetupContract(ABC):
Rows: ``{layout, paper, canonical_media_name, size_mm: [w, h],
orientation, plot_style, scale, plot_area, device,
margins_mm: [top, bottom, left, right], center}``. ``paper`` is the
margins_mm: [top, bottom, left, right], paper_units, center}``.
``size_mm`` and ``margins_mm`` are the sheet *as plotted*: under
``plot_rotation`` 1/3 (AutoCAD's landscape-on-portrait-media
convention) both are turned together, through the one mapping the
renderer plots by (``papers.turned_sheet``), so the printable area a
row implies is the area the same engine plots. ``paper`` is the
catalogue name when the size matches one within 0.5 mm, otherwise the
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.
media name as stored. ``paper_units`` (``"mm"`` | ``"inches"`` |
``"pixels"``) is what one paper-space unit means at the plot scale;
sizes and margins are millimetres whatever it says. ``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,9 +41,19 @@ class PageSetupContract(ABC):
``engineering.standards.papers.resolve_page_setup`` to a layout.
Returns ``{ok, layout, applied: {...setup}, changed: {key: [old, new]},
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
warnings: [str], plot_style_known, viewports_kept}``. ``changed`` is
computed from the read-back before and after (the rows
``page_setup_list`` reports), so re-applying the same setup reports
``{}``. The write resets ``plot_rotation`` to 0 (orientation rides on
the media's width/height); headlessly, with no ``margins_mm`` asked
for, the turned margins are re-written un-turned so the sheet keeps
the margins it had, and on the live engine (margins come from the
.pc3) the ones that moved show in ``changed``. ``setup["paper_units"]``
(``"mm"`` default, ``"inches"``, or ``None`` to keep the layout's
units as AutoCAD's own Page Setup does) is written when given; a move
rescales every paper-space unit by 25.4 at the plot scale and is named
in ``changed`` *and* ``warnings`` (``papers.page_setup_warnings``).
Refuses ``Model``, an unknown layout, and a ``setup`` missing a
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.
+53 -38
View File
@@ -2340,23 +2340,39 @@ class EzdxfBackend(AutoCADBackend):
return doc.layouts.get(resolved)
@staticmethod
def _page_setup_row(layout) -> dict:
def _stored_sheet(dxf) -> tuple[list[float], list[float]]:
"""``(size_mm, margins_mm)`` of the sheet *as plotted*, from the media
as stored: ``papers.turned_sheet`` over PLOTSETTINGS 40-47 and
``plot_rotation``. DXF stores every paper size and margin in
millimetres, whatever ``plot_paper_units`` says (ezdxf's
acdb_plot_settings, verified). ``margins_mm`` is [top, bottom, left, right].
"""
from engineering.standards.papers import turned_sheet
return turned_sheet(
dxf.paper_width,
dxf.paper_height,
(dxf.top_margin, dxf.bottom_margin, dxf.left_margin, dxf.right_margin),
dxf.plot_rotation,
)
@classmethod
def _page_setup_row(cls, layout) -> dict:
from engineering.standards.papers import (
PLOT_TYPE_NAMES,
center_applies,
dxf_scale_label,
paper_from_size,
paper_units_name,
scale_label,
)
dxf = layout.dxf_layout.dxf
flags = int(dxf.get("plot_layout_flags", dxf.get_default("plot_layout_flags")))
# DXF stores every paper size and margin in millimetres, whatever
# plot_paper_units says (ezdxf's acdb_plot_settings, verified).
width, height = float(dxf.paper_width), float(dxf.paper_height)
rotation = int(dxf.plot_rotation)
if rotation in (1, 3):
width, height = height, width
# The sheet as plotted: size *and* margins turned by plot_rotation
# (the same mapping _paper_frame plots by). Swapping only the size
# reported a 297 x 210 sheet with its portrait margins.
(width, height), margins = cls._stored_sheet(dxf)
media = str(dxf.paper_size)
numerator, denominator = float(dxf.scale_numerator), float(dxf.scale_denominator)
if flags & layout.USE_STANDARD_SCALE:
@@ -2374,12 +2390,9 @@ class EzdxfBackend(AutoCADBackend):
"scale": scale,
"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),
round(float(dxf.bottom_margin), 3),
round(float(dxf.left_margin), 3),
round(float(dxf.right_margin), 3),
],
"margins_mm": [round(v, 3) for v in margins],
# What one paper-space unit means at the plot scale (DXF group 72).
"paper_units": paper_units_name(dxf.plot_paper_units),
# 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,
@@ -2460,30 +2473,18 @@ class EzdxfBackend(AutoCADBackend):
shrank the axes box to 0.707 and cut everything past x = 210 minus the
margin (measured: a 277 mm frame lost its right 27%, with ``ok: True``
and a MediaBox that read as correct). The margins turn with the sheet,
mapped the way ezdxf's own ``Page.from_dxf_layout`` maps them. What
the headless renderer still does not do is rotate the *content* on the
media (reported as ``rotation_applied``, not hidden).
mapped the way ezdxf's own ``Page.from_dxf_layout`` maps them —
``papers.turned_sheet``, the one mapping ``page_setup_list`` reports
by, so the row and the plot never disagree. What the headless renderer
still does not do is rotate the *content* on the media (reported as
``rotation_applied``, not hidden).
"""
from engineering.standards.papers import PLOT_TYPE_NAMES, scale_label
dxf = layout.dxf_layout.dxf
width, height = float(dxf.paper_width), float(dxf.paper_height)
rotation = int(dxf.plot_rotation)
unit = 25.4 if int(dxf.plot_paper_units) == 0 else 1.0
top, right = float(dxf.top_margin), float(dxf.right_margin)
bottom, left = float(dxf.bottom_margin), float(dxf.left_margin)
# (top, right, bottom, left) after the turn — ezdxf's Page.from_dxf_layout.
if rotation == 1:
sheet_w, sheet_h = height, width
top, right, bottom, left = right, bottom, left, top
elif rotation == 2:
sheet_w, sheet_h = width, height
top, right, bottom, left = bottom, left, top, right
elif rotation == 3:
sheet_w, sheet_h = height, width
top, right, bottom, left = left, top, right, bottom
else:
sheet_w, sheet_h = width, height
(sheet_w, sheet_h), (top, bottom, left, right) = cls._stored_sheet(dxf)
offset_x = float(dxf.plot_origin_x_offset)
offset_y = float(dxf.plot_origin_y_offset)
printable_w = max(sheet_w - left - right, 1e-9)
@@ -2576,7 +2577,11 @@ class EzdxfBackend(AutoCADBackend):
return await self._async(_sync)
async def page_setup_apply(self, layout: str, setup: dict) -> dict:
from engineering.standards.papers import require_page_setup
from engineering.standards.papers import (
PAPER_UNITS,
page_setup_warnings,
require_page_setup,
)
resolved = require_page_setup(setup)
@@ -2592,17 +2597,26 @@ class EzdxfBackend(AutoCADBackend):
if not dxf.hasattr("plot_layout_flags"):
dxf.plot_layout_flags = dxf.get_default("plot_layout_flags")
width, height = (float(v) for v in resolved["size_mm"])
# Orientation rides on width/height, as AutoCAD names media, so
# plot_rotation goes to 0. The stored margins only meant what
# they meant *through* the old turn: with none asked for, the
# sheet keeps the margins it had (the turned ones are written as
# the unturned ones), instead of silently swapping to the media's
# portrait margins with nothing in ``changed``.
margins = resolved["margins_mm"]
if margins is None:
margins = self._stored_sheet(dxf)[1] # turned, un-rounded
dxf.page_setup_name = ""
dxf.plot_configuration_file = resolved["device"]
dxf.paper_size = resolved["canonical_media_name"]
dxf.paper_width = width
dxf.paper_height = height
dxf.plot_paper_units = 1 # millimetres
dxf.plot_rotation = 0 # orientation rides on width/height, as AutoCAD names media
if resolved["margins_mm"] is not None:
top, bottom, left, right = (float(v) for v in resolved["margins_mm"])
dxf.top_margin, dxf.bottom_margin = top, bottom
dxf.left_margin, dxf.right_margin = left, right
if resolved["paper_units"] is not None:
dxf.plot_paper_units = PAPER_UNITS[resolved["paper_units"]]
dxf.plot_rotation = 0
top, bottom, left, right = (float(v) for v in margins)
dxf.top_margin, dxf.bottom_margin = top, bottom
dxf.left_margin, dxf.right_margin = left, right
dxf.plot_origin_x_offset = 0.0
dxf.plot_origin_y_offset = 0.0
dxf.unit_factor = 1.0
@@ -2639,6 +2653,7 @@ class EzdxfBackend(AutoCADBackend):
"layout": target.name,
"applied": dict(resolved),
"changed": changed,
"warnings": page_setup_warnings(changed),
"plot_style_known": bool(resolved["plot_style_known"]),
"viewports_kept": True,
"backend": "ezdxf",