fix(task-17): the headless sheet export plots at the layout's scale and plot area, and reports both

drawing_export_pdf sized the axes window as the sheet at 1:1 whatever
page_setup_apply had written: a 1:2 sheet exported pixel-identical to
the same sheet at 1:1, ok: True, with a MediaBox that read as correct,
so nothing downstream (batch_plot's mediabox gate included) could see it.

_paper_frame now spans the sheet in paper-space units — sheet_mm / unit
x denominator / numerator, the custom ratio for the labels code 75 lacks
(1:5, 5:1) — and honours the plot area: layout (5) from the plot origin;
extents (1) as the paper-space extents (the main viewport excluded, as
AutoCAD excludes it) at the plot origin or centred, scaled to the
printable area under fit. fit under a layout plot is 1:1, the choice
AutoCAD greys out. display/view/limits/window fall back to the sheet
window and say so. The result carries scale, effective_scale,
scale_applied, plot_area, plot_area_applied (and plot_area_note), the
way rotation_applied is already reported.

Tests look at the ink, since the MediaBox cannot see this: an 800-unit
frame ends at (left + 400) / 420 of the width at 1:2 and runs off the
sheet at 1:1; extents + fit spans the printable width exactly; the
custom 1:5 window is 2100 units; an empty extents plot and a display
plot are reported, not hidden.

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 10:01:49 +03:00
co-authored by Claude Opus 5
parent 376dc812a5
commit 4a6591843f
2 changed files with 278 additions and 10 deletions
+126
View File
@@ -294,6 +294,132 @@ async def test_rotated_layout_renders_the_whole_sheet(backend, tmp_path):
assert abs(mm_w - 297.0) <= 0.5 and abs(mm_h - 210.0) <= 0.5
async def _export_png(backend, tmp_path, layout: str, stem: str):
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
pytest.importorskip("PIL", reason="pixel check needs Pillow")
png = tmp_path / f"{stem}.png"
result = await backend.drawing_export_pdf(str(png), layout=layout)
assert result["ok"] is True, result
return result, _ink_columns(str(png))
async def test_layout_plot_honours_the_plot_scale(backend, tmp_path):
"""A 1:2 sheet holds twice the paper-space units a 1:1 sheet holds.
The MediaBox is the same 420 x 297 either way, so it cannot see this;
the ink can. An 800-unit-wide frame at 1:2 spans 400 mm and ends inside
the sheet; at 1:1 it runs off the right edge. Before the fix the two
exports were pixel-identical (both clipped at the edge) with ``ok: True``.
"""
await _sheet(backend)
await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale="1:2"))
lay = backend._doc.layouts.get(SHEET)
lay.add_lwpolyline([(0, 0), (800, 0), (800, 500), (0, 500)], close=True)
_, _, left_mm, _ = (await backend.page_setup_list(SHEET))[0]["margins_mm"]
frame = backend._paper_frame(lay)
assert frame["scale"] == "1:2" and frame["scale_applied"] is True
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(840.0)
assert frame["ylim"][1] - frame["ylim"][0] == pytest.approx(594.0)
result, (first, last, width) = await _export_png(backend, tmp_path, SHEET, "half")
assert result["scale"] == "1:2" and result["effective_scale"] == "1:2"
assert result["scale_applied"] is True
assert result["plot_area"] == "layout" and result["plot_area_applied"] is True
expected_last = int(width * (left_mm + 400.0) / 420.0)
assert abs(last - expected_last) <= 3, (first, last, width, expected_last)
await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale="1:1"))
result, (_, last_full, _) = await _export_png(backend, tmp_path, SHEET, "full")
assert result["scale"] == "1:1"
assert last_full >= width - 2, "at 1:1 the 800-unit frame runs off the 420 mm sheet"
assert last < last_full - 20, "1:2 and 1:1 must not render the same"
async def test_custom_scale_sizes_the_window_too(backend):
"""1:5 has no DXF code 75, so it rides on the numerator / denominator."""
await _sheet(backend)
await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale="1:5"))
frame = backend._paper_frame(backend._doc.layouts.get(SHEET))
assert frame["scale"] == "1:5" and frame["scale_applied"] is True
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(2100.0)
async def test_fit_on_a_layout_plot_is_one_to_one(backend):
"""AutoCAD greys "Fit to paper" out under the layout plot area."""
await _sheet(backend)
await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale="fit"))
frame = backend._paper_frame(backend._doc.layouts.get(SHEET))
assert frame["scale"] == "fit" and frame["effective_scale"] == "1:1"
assert frame["scale_applied"] is True
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(420.0)
async def test_extents_plot_fits_the_printable_area_and_centres(backend, tmp_path):
"""extents + fit: the paper-space extents (not the main viewport, which
ezdxf's bbox would count) scale to the printable area and sit centred."""
await _sheet(backend)
await backend.page_setup_apply(
SHEET, resolve_page_setup("ISO_A3", scale="fit", plot_area="extents", center=True)
)
lay = backend._doc.layouts.get(SHEET)
lay.add_lwpolyline([(0, 0), (800, 0), (800, 500), (0, 500)], close=True)
top, bottom, left_mm, right_mm = (await backend.page_setup_list(SHEET))[0]["margins_mm"]
printable_w, printable_h = 420.0 - left_mm - right_mm, 297.0 - top - bottom
mm_per_unit = min(printable_w / 800.0, printable_h / 500.0)
frame = backend._paper_frame(lay)
assert frame["plot_area"] == "extents" and frame["plot_area_applied"] is True
assert frame["scale"] == "fit"
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(420.0 / mm_per_unit)
result, (first, last, width) = await _export_png(backend, tmp_path, SHEET, "extents")
assert result["effective_scale"] == scale_label(1.0, 1.0 / mm_per_unit)
# The frame is the wider of the two: it spans the printable width exactly.
assert abs(first - int(width * left_mm / 420.0)) <= 3, (first, last, width)
assert abs(last - int(width * (420.0 - right_mm) / 420.0)) <= 3, (first, last, width)
async def test_extents_plot_at_a_fixed_scale_starts_at_the_plot_origin(backend):
await _sheet(backend)
await backend.page_setup_apply(
SHEET, resolve_page_setup("ISO_A3", scale="1:2", plot_area="extents", center=False)
)
lay = backend._doc.layouts.get(SHEET)
lay.add_lwpolyline([(100, 50), (900, 50), (900, 550), (100, 550)], close=True)
_, _, left_mm, _ = (await backend.page_setup_list(SHEET))[0]["margins_mm"]
frame = backend._paper_frame(lay)
assert frame["effective_scale"] == "1:2" and frame["plot_area_applied"] is True
# The extents' lower-left corner lands on the printable corner: sheet x0
# is that corner less the left margin, in paper-space units at 1:2.
assert frame["xlim"][0] == pytest.approx(100.0 - left_mm * 2.0)
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(840.0)
async def test_extents_plot_of_an_empty_layout_falls_back_to_the_sheet(backend):
await _sheet(backend)
await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", plot_area="extents"))
frame = backend._paper_frame(backend._doc.layouts.get(SHEET))
assert frame["plot_area_applied"] is False
assert "no entities" in frame["plot_area_note"]
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(420.0)
async def test_unrenderable_plot_area_is_reported_not_hidden(backend, tmp_path):
"""display / view need a screen; the headless engine plots the sheet and says so."""
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
await _sheet(backend)
await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale="1:1"))
lay = backend._doc.layouts.get(SHEET)
lay.dxf_layout.dxf.plot_type = 0 # display
lay.add_line((0, 0), (100, 100))
result = await backend.drawing_export_pdf(str(tmp_path / "display.pdf"), layout=SHEET)
assert result["ok"] is True
assert result["plot_area"] == "display" and result["plot_area_applied"] is False
assert "display" in result["plot_area_note"]
assert result["scale_applied"] is True
async def test_apply_materialises_the_default_flags_before_setting_centre(backend):
"""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."""