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:
co-authored by
Claude Opus 5
parent
4a6591843f
commit
f2386a9fdd
+27
-5
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -42,6 +42,8 @@ __all__ = [
|
||||
"ORIENTATIONS",
|
||||
"PAPER_INCHES",
|
||||
"PAPER_SIZES",
|
||||
"PAPER_UNITS",
|
||||
"PAPER_UNITS_NAMES",
|
||||
"PLOT_AREAS",
|
||||
"PLOT_TYPE_LAYOUT",
|
||||
"PLOT_TYPE_NAMES",
|
||||
@@ -51,13 +53,16 @@ __all__ = [
|
||||
"canonical_media_name",
|
||||
"center_applies",
|
||||
"dxf_scale_label",
|
||||
"page_setup_warnings",
|
||||
"paper_from_size",
|
||||
"paper_size_mm",
|
||||
"paper_units_name",
|
||||
"plot_style_known",
|
||||
"require_page_setup",
|
||||
"resolve_page_setup",
|
||||
"scale_label",
|
||||
"scale_ratio",
|
||||
"turned_sheet",
|
||||
]
|
||||
|
||||
#: Portrait width × height in millimetres. Landscape swaps.
|
||||
@@ -262,6 +267,85 @@ def plot_style_known(name) -> bool:
|
||||
return str(name or "").strip().lower() in {entry.lower() for entry in CTB_CATALOG}
|
||||
|
||||
|
||||
#: Plot paper units — the same codes in DXF group 72 (``plot_paper_units``) and
|
||||
#: ActiveX ``AcPlotPaperUnits`` (acInches=0, acMillimeters=1, acPixels=2). The
|
||||
#: paper size and margins are stored in millimetres whatever this says (ezdxf's
|
||||
#: acdb_plot_settings; GetPaperSize / GetPaperMargins measured live): the units
|
||||
#: govern what one paper-space unit *means* at the plot scale — under inches a
|
||||
#: 1:1 sheet holds 17 units across ANSI B, under millimetres 431.8.
|
||||
PAPER_UNITS: dict[str, int] = {"inches": 0, "mm": 1}
|
||||
PAPER_UNITS_NAMES: dict[int, str] = {0: "inches", 1: "mm", 2: "pixels"}
|
||||
_PAPER_UNITS_ALIASES = {"in": "inches", "inch": "inches", "inches": "inches", "mm": "mm"}
|
||||
|
||||
|
||||
def paper_units_name(code) -> str | None:
|
||||
"""Read-back for both engines: ``inches`` / ``mm`` / ``pixels``, ``None``
|
||||
for an unreadable value, ``code:<n>`` for one neither enum names."""
|
||||
if code is None:
|
||||
return None
|
||||
try:
|
||||
value = int(code)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return PAPER_UNITS_NAMES.get(value, f"code:{value}")
|
||||
|
||||
|
||||
def turned_sheet(width_mm, height_mm, margins_mm, rotation) -> tuple[list[float], list[float]]:
|
||||
"""The sheet as plotted from the media as stored: ``(size_mm, margins_mm)``.
|
||||
|
||||
``margins_mm`` in and out is ``[top, bottom, left, right]`` — the row's
|
||||
order. ``plot_rotation`` 1/3 is AutoCAD's landscape-on-portrait-media
|
||||
convention (media 210 x 297, sheet 297 x 210): the size swaps *and* the
|
||||
margins turn with it, mapped the way ezdxf's ``Page.from_dxf_layout``
|
||||
maps them (rotation 1: top<-right, right<-bottom, bottom<-left, left<-top;
|
||||
2: opposite sides; 3: the inverse of 1). Every read-back and the headless
|
||||
renderer go through this one function, so a row's printable area is the
|
||||
area the same engine plots — swapping only the size reported a 297 x 210
|
||||
sheet with [20, 20, 7.5, 7.5] (282 x 170 printable) whose true margins
|
||||
were [7.5, 7.5, 20, 20] (257 x 195).
|
||||
"""
|
||||
width, height = float(width_mm), float(height_mm)
|
||||
top, bottom, left, right = (float(v) for v in margins_mm)
|
||||
turn = int(rotation or 0)
|
||||
if turn == 1:
|
||||
width, height = height, width
|
||||
top, right, bottom, left = right, bottom, left, top
|
||||
elif turn == 2:
|
||||
top, right, bottom, left = bottom, left, top, right
|
||||
elif turn == 3:
|
||||
width, height = height, width
|
||||
top, right, bottom, left = left, top, right, bottom
|
||||
return [width, height], [top, bottom, left, right]
|
||||
|
||||
|
||||
def page_setup_warnings(changed: dict) -> list[str]:
|
||||
"""What a ``page_setup_apply`` moved that ``changed`` alone under-states.
|
||||
|
||||
A paper-units move is a 25.4x rescale of every paper-space unit at the
|
||||
plot scale (a 16-unit frame on an inch sheet spans 406.4 mm of paper;
|
||||
after a move to millimetres, 16 mm) with ``ok: True`` and the scale
|
||||
reported as applied — so it is named here, on both engines, with the
|
||||
way back. Both engines build the same list from the same ``changed``.
|
||||
"""
|
||||
warnings: list[str] = []
|
||||
move = changed.get("paper_units")
|
||||
if move and move[0] is not None and move[0] != move[1]:
|
||||
old, new = move
|
||||
if (old, new) == ("inches", "mm"):
|
||||
effect = "plots 25.4x smaller"
|
||||
elif (old, new) == ("mm", "inches"):
|
||||
effect = "plots 25.4x larger"
|
||||
else:
|
||||
effect = "plots at a different size"
|
||||
warnings.append(
|
||||
f"paper units moved from {old} to {new}: one paper-space unit now plots as one "
|
||||
f"{new} at the plot scale, so paper-space geometry authored in {old} {effect} "
|
||||
f"at the same scale; pass paper_units=None to keep a layout's units or "
|
||||
f"paper_units={old!r} to put them back"
|
||||
)
|
||||
return warnings
|
||||
|
||||
|
||||
def center_applies(plot_type) -> bool:
|
||||
"""Whether "centre the plot" means anything under this ``AcPlotType``.
|
||||
|
||||
@@ -284,6 +368,7 @@ def resolve_page_setup(
|
||||
device: str = "DWG To PDF.pc3",
|
||||
margins_mm=None,
|
||||
center: bool = True,
|
||||
paper_units: str | None = "mm",
|
||||
) -> dict:
|
||||
"""Validate a page setup request and resolve it to what both engines write.
|
||||
|
||||
@@ -292,6 +377,13 @@ def resolve_page_setup(
|
||||
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.
|
||||
|
||||
``paper_units`` is what one paper-space unit means at the plot scale:
|
||||
``"mm"`` (the default — this server draws in millimetres), ``"inches"``,
|
||||
or ``None`` to keep the layout's current units, which is what AutoCAD's
|
||||
own Page Setup does when a media is chosen. A write that moves the units
|
||||
rescales every paper-space unit by 25.4; both engines report it in
|
||||
``changed`` and ``warnings``.
|
||||
"""
|
||||
name = _paper_key(paper)
|
||||
width, height = paper_size_mm(name, _orientation(orientation))
|
||||
@@ -337,6 +429,15 @@ def resolve_page_setup(
|
||||
raise ValueError(f"center: must be true or false, got {center!r}")
|
||||
plot_type = PLOT_AREAS[plot_area]
|
||||
|
||||
units = None
|
||||
if paper_units is not None:
|
||||
units = _PAPER_UNITS_ALIASES.get(str(paper_units).strip().lower())
|
||||
if units is None:
|
||||
raise ValueError(
|
||||
f"paper_units: must be 'mm', 'inches' or None (keep the layout's units), "
|
||||
f"got {paper_units!r}"
|
||||
)
|
||||
|
||||
return {
|
||||
"paper": name,
|
||||
"orientation": orientation,
|
||||
@@ -354,6 +455,7 @@ def resolve_page_setup(
|
||||
"device": device_name,
|
||||
"margins_mm": margins,
|
||||
"center": center if center_applies(plot_type) else None,
|
||||
"paper_units": units,
|
||||
}
|
||||
|
||||
|
||||
@@ -375,6 +477,7 @@ REQUIRED_SETUP_KEYS: tuple[str, ...] = (
|
||||
"device",
|
||||
"margins_mm",
|
||||
"center",
|
||||
"paper_units",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@ from engineering.standards.papers import (
|
||||
SCALES,
|
||||
canonical_media_name,
|
||||
paper_from_size,
|
||||
paper_units_name,
|
||||
resolve_page_setup,
|
||||
scale_label,
|
||||
scale_ratio,
|
||||
turned_sheet,
|
||||
)
|
||||
from engineering.standards.pdfinfo import read_mediabox
|
||||
|
||||
@@ -146,6 +148,59 @@ def test_unknown_ctb_is_allowed_but_flagged():
|
||||
assert setup["plot_style"] == "company.ctb" and setup["plot_style_known"] is False
|
||||
|
||||
|
||||
def test_resolve_page_setup_paper_units_default_mm_and_keep():
|
||||
assert resolve_page_setup("ISO_A3")["paper_units"] == "mm"
|
||||
assert resolve_page_setup("ANSI_B", paper_units="inches")["paper_units"] == "inches"
|
||||
assert resolve_page_setup("ANSI_B", paper_units="inch")["paper_units"] == "inches"
|
||||
assert resolve_page_setup("ANSI_B", paper_units=None)["paper_units"] is None, "keep"
|
||||
with pytest.raises(ValueError, match="paper_units"):
|
||||
resolve_page_setup("ISO_A3", paper_units="cm")
|
||||
|
||||
|
||||
def test_paper_units_name_covers_the_three_dxf_and_activex_codes():
|
||||
assert paper_units_name(0) == "inches" and paper_units_name(1) == "mm"
|
||||
assert paper_units_name(2) == "pixels"
|
||||
assert paper_units_name(None) is None and paper_units_name(7) == "code:7"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rotation, size, margins",
|
||||
[
|
||||
# stored media 210 x 297 with (top, bottom, left, right) = (1, 3, 4, 2),
|
||||
# i.e. ezdxf's clockwise (top, right, bottom, left) = (1, 2, 3, 4).
|
||||
(0, [210.0, 297.0], [1.0, 3.0, 4.0, 2.0]),
|
||||
(1, [297.0, 210.0], [2.0, 4.0, 1.0, 3.0]),
|
||||
(2, [210.0, 297.0], [3.0, 1.0, 2.0, 4.0]),
|
||||
(3, [297.0, 210.0], [4.0, 2.0, 3.0, 1.0]),
|
||||
],
|
||||
)
|
||||
def test_turned_sheet_maps_size_and_margins_like_ezdxf_page_from_dxf_layout(
|
||||
rotation, size, margins
|
||||
):
|
||||
"""The one mapping ``page_setup_list``, ``_paper_frame`` and the COM row share.
|
||||
|
||||
Cross-checked against ezdxf's ``Page.from_dxf_layout`` (the renderer's own
|
||||
reading of PLOTSETTINGS 40-43 under ``plot_rotation``), so the row a
|
||||
client reads is the sheet the same engine plots.
|
||||
"""
|
||||
assert turned_sheet(210, 297, [1, 3, 4, 2], rotation) == (size, margins)
|
||||
|
||||
|
||||
def test_turned_sheet_agrees_with_ezdxf_page_from_dxf_layout():
|
||||
ezdxf = pytest.importorskip("ezdxf")
|
||||
from ezdxf.addons.drawing.layout import Page
|
||||
|
||||
doc = ezdxf.new()
|
||||
lay = doc.layouts.new("Turn")
|
||||
for rotation in (0, 1, 2, 3):
|
||||
lay.page_setup(size=(210, 297), margins=(1, 2, 3, 4), units="mm", rotation=rotation)
|
||||
page = Page.from_dxf_layout(lay.dxf_layout)
|
||||
size, (top, bottom, left, right) = turned_sheet(210, 297, [1, 3, 4, 2], rotation)
|
||||
assert size == [page.width, page.height], rotation
|
||||
assert (page.margins.top, page.margins.bottom) == (top, bottom), rotation
|
||||
assert (page.margins.left, page.margins.right) == (left, right), rotation
|
||||
|
||||
|
||||
# ── the PDF reader ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -525,6 +580,170 @@ async def test_list_covers_every_paper_layout_and_skips_model(backend):
|
||||
assert "Model" not in names and {"A", "B"} <= set(names)
|
||||
|
||||
|
||||
def _rotated_a4(backend, name: str = "Rot", rotation: int = 1):
|
||||
"""AutoCAD's landscape-on-portrait-media convention: media 210 x 297,
|
||||
ezdxf clockwise margins (top, right, bottom, left) = (20, 7.5, 20, 7.5),
|
||||
turned by ``plot_rotation``."""
|
||||
lay = backend._doc.layouts.get(name)
|
||||
lay.page_setup(
|
||||
size=(210, 297),
|
||||
margins=(20, 7.5, 20, 7.5),
|
||||
units="mm",
|
||||
rotation=rotation,
|
||||
scale=16,
|
||||
name="ISO_A4",
|
||||
device="DWG To PDF.pc3",
|
||||
)
|
||||
return lay
|
||||
|
||||
|
||||
async def test_list_turns_the_margins_with_the_sheet(backend):
|
||||
"""The row used to swap ``size_mm`` for rotation 1/3 but report the
|
||||
margins un-turned: [20, 20, 7.5, 7.5] on a 297 x 210 sheet whose true
|
||||
margins (ezdxf's Page, and the window ``_paper_frame`` plots) are
|
||||
[7.5, 7.5, 20, 20] — a printable area of 282 x 170 from the row against
|
||||
257 x 195 on paper."""
|
||||
await _sheet(backend, "Rot")
|
||||
lay = _rotated_a4(backend)
|
||||
row = (await backend.page_setup_list("Rot"))[0]
|
||||
assert row["size_mm"] == [297.0, 210.0] and row["orientation"] == "landscape"
|
||||
assert row["margins_mm"] == [7.5, 7.5, 20.0, 20.0]
|
||||
top, bottom, left, right = row["margins_mm"]
|
||||
assert (297 - left - right, 210 - top - bottom) == (257.0, 195.0)
|
||||
|
||||
# The same sheet the renderer plots: at 1:1 the axes window starts one
|
||||
# (turned) left margin before x = 0 and one (turned) bottom margin below y = 0.
|
||||
frame = backend._paper_frame(lay)
|
||||
assert frame["size_mm"] == row["size_mm"]
|
||||
assert frame["xlim"][0] == pytest.approx(-left) and frame["ylim"][0] == pytest.approx(-bottom)
|
||||
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(297.0)
|
||||
assert frame["ylim"][1] - frame["ylim"][0] == pytest.approx(210.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rotation", [1, 2, 3])
|
||||
async def test_apply_on_a_turned_sheet_keeps_the_effective_margins(backend, rotation):
|
||||
"""``page_setup_apply`` writes ``plot_rotation`` 0 (orientation rides on
|
||||
width/height). With no ``margins_mm`` asked for, the stored margins used
|
||||
to stay as they were while the turn that gave them their meaning went
|
||||
away, so the effective margins moved from [7.5, 7.5, 20, 20] to
|
||||
[20, 20, 7.5, 7.5] (printable 257 x 195 -> 282 x 170) and ``changed``
|
||||
said nothing. The stored margins are now re-mapped so the sheet keeps
|
||||
the margins it had, and the row before equals the row after."""
|
||||
await _sheet(backend, "Rot")
|
||||
lay = _rotated_a4(backend, rotation=rotation)
|
||||
before = (await backend.page_setup_list("Rot"))[0]
|
||||
result = await backend.page_setup_apply(
|
||||
"Rot", resolve_page_setup("ISO_A4", before["orientation"], scale="1:1")
|
||||
)
|
||||
after = (await backend.page_setup_list("Rot"))[0]
|
||||
assert int(lay.dxf_layout.dxf.plot_rotation) == 0
|
||||
assert after["margins_mm"] == before["margins_mm"]
|
||||
assert after["size_mm"] == before["size_mm"]
|
||||
assert "margins_mm" not in result["changed"] and "size_mm" not in result["changed"]
|
||||
dxf = lay.dxf_layout.dxf
|
||||
top, bottom, left, right = after["margins_mm"]
|
||||
assert (dxf.top_margin, dxf.bottom_margin, dxf.left_margin, dxf.right_margin) == (
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
)
|
||||
|
||||
|
||||
async def test_apply_with_margins_on_a_turned_sheet_writes_them_as_asked(backend):
|
||||
await _sheet(backend, "Rot")
|
||||
_rotated_a4(backend)
|
||||
result = await backend.page_setup_apply(
|
||||
"Rot", resolve_page_setup("ISO_A4", "landscape", margins_mm=[5, 6, 7, 8])
|
||||
)
|
||||
row = (await backend.page_setup_list("Rot"))[0]
|
||||
assert row["margins_mm"] == [5.0, 6.0, 7.0, 8.0]
|
||||
assert result["changed"]["margins_mm"] == [[7.5, 7.5, 20.0, 20.0], [5.0, 6.0, 7.0, 8.0]]
|
||||
|
||||
|
||||
def _inch_sheet(backend, name: str = "In"):
|
||||
"""An inch-authored ANSI B sheet (what ANSI_A-E exist for): paper-space
|
||||
units are inches, so at 1:1 a 16-unit frame spans 16 inches of paper."""
|
||||
lay = backend._doc.layouts.get(name)
|
||||
lay.page_setup(
|
||||
size=(17, 11),
|
||||
margins=(0.5, 0.5, 0.5, 0.5),
|
||||
units="inch",
|
||||
scale=16,
|
||||
name="ANSI_B",
|
||||
device="DWG To PDF.pc3",
|
||||
)
|
||||
lay.add_lwpolyline([(0, 0), (16, 0), (16, 10), (0, 10)], close=True)
|
||||
return lay
|
||||
|
||||
|
||||
async def test_row_reports_the_paper_units(backend):
|
||||
await _sheet(backend, "In")
|
||||
lay = _inch_sheet(backend)
|
||||
row = (await backend.page_setup_list("In"))[0]
|
||||
assert row["paper_units"] == "inches"
|
||||
assert row["size_mm"] == [431.8, 279.4] and row["paper"] == "ANSI_B"
|
||||
assert row["margins_mm"] == [12.7, 12.7, 12.7, 12.7]
|
||||
lay.dxf_layout.dxf.plot_paper_units = 1
|
||||
assert (await backend.page_setup_list("In"))[0]["paper_units"] == "mm"
|
||||
|
||||
|
||||
async def test_apply_names_a_paper_units_flip_and_its_consequence(backend):
|
||||
"""The default setup plots in millimetres. On an inch-authored sheet that
|
||||
is a 25.4x rescale of every paper-space unit: the 16-unit frame used to
|
||||
go from 406.4 mm to 16 mm of paper with ``changed`` naming only the
|
||||
media and the plot style — ``ok: True``, ``scale_applied: True``."""
|
||||
await _sheet(backend, "In")
|
||||
lay = _inch_sheet(backend)
|
||||
result = await backend.page_setup_apply(
|
||||
"In", resolve_page_setup("ANSI_B", "landscape", scale="1:1")
|
||||
)
|
||||
assert result["changed"]["paper_units"] == ["inches", "mm"]
|
||||
assert int(lay.dxf_layout.dxf.plot_paper_units) == 1
|
||||
assert result["warnings"], "a units flip is never silent"
|
||||
assert any("inches" in w and "mm" in w and "25.4" in w for w in result["warnings"])
|
||||
assert result["applied"]["paper_units"] == "mm"
|
||||
# Re-applying the same setup moves nothing and warns of nothing.
|
||||
again = await backend.page_setup_apply(
|
||||
"In", resolve_page_setup("ANSI_B", "landscape", scale="1:1")
|
||||
)
|
||||
assert again["changed"] == {} and again["warnings"] == []
|
||||
|
||||
|
||||
async def test_apply_can_keep_or_set_the_paper_units(backend):
|
||||
await _sheet(backend, "In")
|
||||
lay = _inch_sheet(backend)
|
||||
kept = await backend.page_setup_apply(
|
||||
"In", resolve_page_setup("ANSI_B", "landscape", scale="1:1", paper_units=None)
|
||||
)
|
||||
assert int(lay.dxf_layout.dxf.plot_paper_units) == 0
|
||||
assert "paper_units" not in kept["changed"] and kept["warnings"] == []
|
||||
assert kept["applied"]["paper_units"] is None
|
||||
# The catalogue's ANSI_B is 432 x 279 mm (ANSI Y14.1's metric value), so
|
||||
# at 1:1 under inches the sheet holds 432 / 25.4 units across.
|
||||
frame = backend._paper_frame(lay)
|
||||
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(432 / 25.4), "inches at 1:1"
|
||||
|
||||
to_mm = await backend.page_setup_apply(
|
||||
"In", resolve_page_setup("ANSI_B", "landscape", scale="1:1", paper_units="mm")
|
||||
)
|
||||
assert to_mm["changed"]["paper_units"] == ["inches", "mm"] and to_mm["warnings"]
|
||||
frame = backend._paper_frame(lay)
|
||||
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(432.0), "432 mm at 1:1"
|
||||
|
||||
back = await backend.page_setup_apply(
|
||||
"In", resolve_page_setup("ANSI_B", "landscape", scale="1:1", paper_units="inches")
|
||||
)
|
||||
assert back["changed"]["paper_units"] == ["mm", "inches"] and back["warnings"]
|
||||
assert int(lay.dxf_layout.dxf.plot_paper_units) == 0
|
||||
|
||||
|
||||
async def test_apply_on_a_mm_sheet_warns_of_nothing(backend):
|
||||
await _sheet(backend)
|
||||
result = await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3"))
|
||||
assert result["warnings"] == [] and "paper_units" not in result["changed"]
|
||||
|
||||
|
||||
async def test_plot_style_list_headless_is_the_catalog(backend):
|
||||
rows = await backend.plot_style_list()
|
||||
assert [row["name"] for row in rows] == list(CTB_CATALOG)
|
||||
@@ -914,3 +1133,76 @@ async def test_com_declares_dwt_write():
|
||||
from backends.com_backend import ComBackend
|
||||
|
||||
assert ComBackend().capabilities().to_dict()["features"]["dwt_write"]["supported"] is True
|
||||
|
||||
|
||||
async def test_com_list_reports_the_paper_units(com_backend):
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
assert (await backend.page_setup_list("Layout1"))[0]["paper_units"] == "mm"
|
||||
layout.PaperUnits = 0
|
||||
assert (await backend.page_setup_list("Layout1"))[0]["paper_units"] == "inches"
|
||||
|
||||
|
||||
async def test_com_list_turns_the_margins_with_a_rotated_sheet(com_backend):
|
||||
"""GetPaperMargins reads PLOTSETTINGS 40-43 — the media's own frame, the
|
||||
fields the DXF row reads — so a rotated sheet turns them the same way."""
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
layout.PlotRotation = 1
|
||||
row = (await backend.page_setup_list("Layout1"))[0]
|
||||
assert row["size_mm"] == [297.0, 210.0]
|
||||
# fake margins: left 7.5, bottom 20, right 7.5, top 20 -> rotation 1:
|
||||
# top=right, right=bottom, bottom=left, left=top -> [7.5, 7.5, 20, 20].
|
||||
assert row["margins_mm"] == [7.5, 7.5, 20.0, 20.0]
|
||||
layout.PlotRotation = 0
|
||||
assert (await backend.page_setup_list("Layout1"))[0]["margins_mm"] == [20.0, 20.0, 7.5, 7.5]
|
||||
|
||||
|
||||
async def test_com_apply_names_a_paper_units_flip(com_backend):
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
layout.PaperUnits = 0
|
||||
layout.log.clear()
|
||||
result = await backend.page_setup_apply(
|
||||
"Layout1", resolve_page_setup("ANSI_B", "landscape", scale="1:1")
|
||||
)
|
||||
assert ("set", "PaperUnits", 1) in layout.log
|
||||
assert result["changed"]["paper_units"] == ["inches", "mm"]
|
||||
assert any("25.4" in w for w in result["warnings"])
|
||||
again = await backend.page_setup_apply(
|
||||
"Layout1", resolve_page_setup("ANSI_B", "landscape", scale="1:1")
|
||||
)
|
||||
assert again["changed"] == {} and again["warnings"] == []
|
||||
|
||||
|
||||
async def test_com_apply_keeps_or_sets_the_paper_units(com_backend):
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
layout.PaperUnits = 0
|
||||
layout.log.clear()
|
||||
kept = await backend.page_setup_apply(
|
||||
"Layout1", resolve_page_setup("ANSI_B", "landscape", paper_units=None)
|
||||
)
|
||||
assert not [entry for entry in layout.log if entry[:2] == ("set", "PaperUnits")]
|
||||
assert layout.PaperUnits == 0 and "paper_units" not in kept["changed"]
|
||||
assert kept["warnings"] == []
|
||||
layout.log.clear()
|
||||
inches = await backend.page_setup_apply(
|
||||
"Layout1", resolve_page_setup("ANSI_B", "landscape", paper_units="inches")
|
||||
)
|
||||
assert ("set", "PaperUnits", 0) in layout.log and inches["changed"] == {}
|
||||
assert inches["warnings"] == []
|
||||
|
||||
|
||||
async def test_com_apply_on_a_rotated_sheet_reports_the_margins_that_moved(com_backend):
|
||||
"""COM cannot write margins (they come from the .pc3), so resetting
|
||||
PlotRotation on a turned sheet moves the effective margins — and the row
|
||||
now says so instead of listing only the media."""
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
layout.PlotRotation = 1
|
||||
result = await backend.page_setup_apply(
|
||||
"Layout1", resolve_page_setup("ISO_A3", "landscape", scale="1:1")
|
||||
)
|
||||
assert result["changed"]["margins_mm"] == [[7.5, 7.5, 20.0, 20.0], [20.0, 20.0, 7.5, 7.5]]
|
||||
assert result["changed"]["size_mm"] == [[297.0, 210.0], [420.0, 297.0]]
|
||||
|
||||
Reference in New Issue
Block a user