From aeb27c252b91e517c41d47f369e6b45ec07bb305 Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Thu, 17 Sep 2026 15:29:47 +0300 Subject: [PATCH] fix(task-17): the headless page setup writes the scale AutoCAD plots by, and reads the code 75 AutoCAD writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two measured root causes on the headless engine, both silent: 1. page_setup_apply wrote PLOTSETTINGS group 147 (unit_factor) as 1.0 for every scale. Under USE_STANDARD_SCALE AutoCAD's plot engine scales by group 147 alone (a 1:10 sheet with code 21 and 147 = 1.0 plotted a 100-unit line at 99.99 mm while the row, `applied` and 142/143 said 1:10; patching 147 to 0.1 alone gave 9.99 mm). AutoCAD writes 147 as numerator / denominator (measured live on AutoCAD 2026 for fit, 1:1, 1:2, 1:10, 1:50, 2:1, 10:1: 1.0, 1.0, 0.5, 0.1, 0.02, 2.0, 10.0). The writer now stores 75, 142/143 and 147 from the one resolved ratio on every route, so no reader can disagree with the plot. 2. DXF_STANDARD_SCALE_TYPE and _plot_scale followed ezdxf's STD_SCALES, which has no 19 = 1:5 and numbers everything above 18 one lower than the ObjectARX StdScaleType enum AutoCAD actually writes (1:10 = 21, 1:20 = 23, 1:50 = 26, 1:100 = 27, 2:1 = 28, 10:1 = 31 — measured via COM SaveAs DXF). An AutoCAD-authored 1:50 sheet listed as "1:100" and a 1:10 sheet rendered at 1:16. papers.py now authors the full file enum (DXF_STD_SCALES, codes 1-33 with AutoCAD's imperial names), the writer uses the same codes ActiveX uses, and nothing goes through ezdxf's table. 1:5 stays on the custom route on both engines (the live setter refuses 19) while the reader knows code 19 for files AutoCAD wrote. Tests: the plan's `1:50 -> 25` assertion was the stale numbering and now pins 26; a pure-data test pins the enum against the measured codes and the ActiveX numbering; a parametrised writer test checks 75/142/143/147 for every catalogue scale; a reader test stores the scale exactly as AutoCAD saves it (including codes 19 and 22 the writer never emits) and checks page_setup_list and _paper_frame report and render it at its own scale. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- backends/ezdxf_backend.py | 31 ++++--- engineering/standards/papers.py | 141 +++++++++++++++++++++++++----- tests/test_page_setup.py | 148 ++++++++++++++++++++++++++++++-- 3 files changed, 281 insertions(+), 39 deletions(-) diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index 61c09e6..3d8bad8 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -2405,12 +2405,12 @@ class EzdxfBackend(AutoCADBackend): Reads the same bits ``_page_setup_row`` reports, so the label here is the label ``page_setup_list`` shows: a standard code under - ``USE_STANDARD_SCALE`` resolves through ezdxf's ``STD_SCALES`` table, + ``USE_STANDARD_SCALE`` resolves through the authored ``DXF_STD_SCALES`` + (the ObjectARX enum AutoCAD writes — ezdxf's ``STD_SCALES`` is one off + above 18 and would render an AutoCAD-authored 1:10 sheet at 1:16), anything else through the stored numerator / denominator. """ - from ezdxf.lldxf.const import STD_SCALES - - from engineering.standards.papers import dxf_scale_label, scale_label + from engineering.standards.papers import DXF_STD_SCALES, dxf_scale_label, scale_label dxf = layout.dxf_layout.dxf flags = int(dxf.get("plot_layout_flags", dxf.get_default("plot_layout_flags"))) @@ -2419,8 +2419,8 @@ class EzdxfBackend(AutoCADBackend): code = int(dxf.standard_scale_type) if code == 0: return "fit", None, None - if code in STD_SCALES: - numerator, denominator = (float(v) for v in STD_SCALES[code]) + if code in DXF_STD_SCALES: + numerator, denominator = (float(v) for v in DXF_STD_SCALES[code]) return dxf_scale_label(code, numerator, denominator), numerator, denominator return scale_label(numerator, denominator), numerator, denominator @@ -2586,8 +2586,6 @@ class EzdxfBackend(AutoCADBackend): resolved = require_page_setup(setup) def _sync(): - from ezdxf.lldxf.const import STD_SCALES - target = self._paper_layout(layout, "page_setup_apply") before = self._page_setup_row(target) dxf = target.dxf_layout.dxf @@ -2619,21 +2617,28 @@ class EzdxfBackend(AutoCADBackend): 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 dxf.current_style_sheet = resolved["plot_style"] target.use_plot_styles(True) target.set_plot_type(int(resolved["plot_type"])) + # The scale is stored three ways and AutoCAD reads a different + # one per route (measured, AutoCAD 2026 — papers.py docstring): + # under USE_STANDARD_SCALE its plot engine scales by group 147 + # alone (a sheet with code 21 and 147 = 1.0 plots 1:10 geometry + # at 1:1 while page_setup_list says 1:10); under the custom + # route it scales by 142/143. All three are written, on every + # route, from the one resolved ratio, so no reader can disagree + # with the plot. ``fit`` has ratio 1/1 and 147 = 1.0, as AutoCAD + # writes it. code = resolved["dxf_standard_scale_type"] + numerator, denominator = (float(v) for v in resolved["scale_ratio"]) if code is not None: dxf.standard_scale_type = int(code) - numerator, denominator = STD_SCALES.get(int(code), (1.0, 1.0)) - dxf.scale_numerator, dxf.scale_denominator = float(numerator), float(denominator) target.use_standard_scale(True) else: - numerator, denominator = (float(v) for v in resolved["scale_ratio"]) dxf.standard_scale_type = 16 - dxf.scale_numerator, dxf.scale_denominator = numerator, denominator target.use_standard_scale(False) + dxf.scale_numerator, dxf.scale_denominator = numerator, denominator + dxf.unit_factor = numerator / denominator # 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. diff --git a/engineering/standards/papers.py b/engineering/standards/papers.py index 2da375a..d9279d2 100644 --- a/engineering/standards/papers.py +++ b/engineering/standards/papers.py @@ -3,18 +3,40 @@ Sources: ISO 216 (A series), ASME/ANSI Y14.1 (A–E, metric millimetre values as AutoCAD's ``ANSI_*`` media measure, inch values as the media are *named*), the ctb files AutoCAD ships in its Plot Styles folder, and the standard-scale -tables of two enums that do not agree with each other: +enum — one numbering, shared by the file and by ActiveX: -* DXF group code 75 (``standard_scale_type``, what ezdxf writes): 16=1:1, - 17=1:2, 20=1:10, 22=1:20, 25=1:50, 26=1:100, 27=2:1, 30=10:1 — no 1:5. -* ActiveX ``AcPlotScale`` (``Layout.StandardScale``, AutoCAD 2026 typelib): - ac1_1=16, ac1_2=17, ac1_10=21, ac1_20=23, ac1_50=26, ac1_100=27, ac2_1=28, - ac10_1=31, acScaleToFit=0. The typelib also declares ``ac1_5=19``, but - AutoCAD 2026 refuses ``Layout.StandardScale = 19`` with "Invalid input" in - every condition tried (PaperUnits mm and inches, UseStandardScale True and - False; measured live 2026-09-17) — its standard-scale list has no 1:5, the - same gap as DXF code 75 — so 1:5 is *not* in the table and takes the custom - route on the live engine too (``SetCustomScale(1, 5)`` reads back ``1:5``). +* DXF group code 75 (``standard_scale_type``) is the ObjectARX + ``AcDbPlotSettings::StdScaleType`` enum: 0=fit, 1..15 the imperial + architectural scales, 16=1:1, 17=1:2, 18=1:4, 19=1:5, 20=1:8, 21=1:10, + 22=1:16, 23=1:20, 24=1:30, 25=1:40, 26=1:50, 27=1:100, 28=2:1, 29=4:1, + 30=8:1, 31=10:1, 32=100:1, 33=1000:1 (``DXF_STD_SCALES``). Measured live + on AutoCAD 2026 (COM ``SaveAs(..., 61)`` after ``StandardScale = n``): + 1:10 saves as 21, 1:50 as 26, 2:1 as 28, 10:1 as 31. ezdxf's + ``STD_SCALES`` table (and the DXF Reference page it follows) has no 19=1:5 + and numbers everything above 18 one lower (20=1:10, 25=1:50, 30=10:1) — + reading an AutoCAD-authored 1:50 sheet through it reports 1:100, and a + 1:10 sheet renders at 1:16 — so nothing here goes through that table. +* DXF group code 147 (``unit_factor`` in ezdxf; "a floating point scale + factor that represents the standard scale value specified in code 75" in + the DXF Reference — the Reference is right) is the factor AutoCAD's plot + engine *uses* under ``USE_STANDARD_SCALE`` (bit 16): AutoCAD writes it as + numerator / denominator (0.1 for 1:10, 2.0 for 2:1, 1.0 for fit and 1:1), + and a file with code 21 but 147 = 1.0 plots at 1:1 while claiming 1:10 + (measured: a 100-unit line spans 99.99 mm of paper). Under the custom + route (bit 16 cleared) AutoCAD honours 142/143 and leaves 75 and 147 + stale. The headless writer therefore writes 147 alongside 142/143 on + every route. +* ActiveX ``AcPlotScale`` (``Layout.StandardScale``, AutoCAD 2026 typelib) + is the same numbering (ac1_1=16, ac1_2=17, ac1_10=21, ac1_20=23, + ac1_50=26, ac1_100=27, ac2_1=28, ac10_1=31, acScaleToFit=0). The typelib + declares ``ac1_5=19`` too, but AutoCAD 2026 refuses + ``Layout.StandardScale = 19`` with "Invalid input" in every condition tried + (PaperUnits mm and inches, UseStandardScale True and False; measured live + 2026-09-17), so 1:5 is not in ``ACTIVEX_PLOT_SCALE`` and takes the custom + route on the live engine (``SetCustomScale(1, 5)`` reads back ``1:5``). + The headless writer mirrors that — 1:5 is not in ``DXF_STANDARD_SCALE_TYPE`` + either, so both engines store the same 1:5 (custom, 142/143 = 1/5) — while + the reader knows code 19 for files AutoCAD wrote. 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. @@ -39,6 +61,7 @@ __all__ = [ "ACTIVEX_PLOT_SCALE", "CTB_CATALOG", "DXF_STANDARD_SCALE_TYPE", + "DXF_STD_SCALES", "ORIENTATIONS", "PAPER_INCHES", "PAPER_SIZES", @@ -126,19 +149,79 @@ SCALES: dict[str, float | None] = { "10:1": 10.0, } -#: DXF group code 75. Labels missing here use the custom numerator/denominator. +#: DXF group code 75 → (numerator, denominator): the ObjectARX +#: ``AcDbPlotSettings::StdScaleType`` enum, which is what AutoCAD writes and +#: reads (module docstring — not ezdxf's ``STD_SCALES``, which is one off +#: above 18). Code 0 is *scaled to fit* and has no ratio. +DXF_STD_SCALES: dict[int, tuple[float, float]] = { + 1: (1.0 / 128.0, 12.0), + 2: (1.0 / 64.0, 12.0), + 3: (1.0 / 32.0, 12.0), + 4: (1.0 / 16.0, 12.0), + 5: (3.0 / 32.0, 12.0), + 6: (1.0 / 8.0, 12.0), + 7: (3.0 / 16.0, 12.0), + 8: (1.0 / 4.0, 12.0), + 9: (3.0 / 8.0, 12.0), + 10: (1.0 / 2.0, 12.0), + 11: (3.0 / 4.0, 12.0), + 12: (1.0, 12.0), + 13: (3.0, 12.0), + 14: (6.0, 12.0), + 15: (12.0, 12.0), + 16: (1.0, 1.0), + 17: (1.0, 2.0), + 18: (1.0, 4.0), + 19: (1.0, 5.0), + 20: (1.0, 8.0), + 21: (1.0, 10.0), + 22: (1.0, 16.0), + 23: (1.0, 20.0), + 24: (1.0, 30.0), + 25: (1.0, 40.0), + 26: (1.0, 50.0), + 27: (1.0, 100.0), + 28: (2.0, 1.0), + 29: (4.0, 1.0), + 30: (8.0, 1.0), + 31: (10.0, 1.0), + 32: (100.0, 1.0), + 33: (1000.0, 1.0), +} +#: AutoCAD's names for the imperial codes (inches of paper = feet of drawing). +_DXF_IMPERIAL_LABELS: dict[int, str] = { + 1: "1/128\"=1'", + 2: "1/64\"=1'", + 3: "1/32\"=1'", + 4: "1/16\"=1'", + 5: "3/32\"=1'", + 6: "1/8\"=1'", + 7: "3/16\"=1'", + 8: "1/4\"=1'", + 9: "3/8\"=1'", + 10: "1/2\"=1'", + 11: "3/4\"=1'", + 12: "1\"=1'", + 13: "3\"=1'", + 14: "6\"=1'", + 15: "1'=1'", +} + +#: DXF group code 75 the headless *writer* uses per catalogue label. Labels +#: missing here use the custom numerator/denominator: ``1:5`` (code 19 exists +#: in the file enum, but the live engine cannot set it, so both engines store +#: 1:5 the same custom way) and ``5:1`` (no code anywhere). DXF_STANDARD_SCALE_TYPE: dict[str, int] = { "fit": 0, "1:1": 16, "1:2": 17, - "1:10": 20, - "1:20": 22, - "1:50": 25, - "1:100": 26, - "2:1": 27, - "10:1": 30, + "1:10": 21, + "1:20": 23, + "1:50": 26, + "1:100": 27, + "2:1": 28, + "10:1": 31, } -_DXF_SCALE_LABEL = {code: label for label, code in DXF_STANDARD_SCALE_TYPE.items()} #: ActiveX ``AcPlotScale`` (AutoCAD 2026 typelib). Labels missing here use SetCustomScale. #: No ``"1:5"``: the typelib's ``ac1_5 = 19`` is refused by AutoCAD 2026 (module docstring). @@ -253,9 +336,25 @@ def scale_label(numerator, denominator) -> str: return f"{num:g}:{den:g}" +_DXF_SCALE_LABEL: dict[int, str] = { + 0: "fit", + **_DXF_IMPERIAL_LABELS, + **{ + code: scale_label(num, den) + for code, (num, den) in DXF_STD_SCALES.items() + if code not in _DXF_IMPERIAL_LABELS + }, +} + + def dxf_scale_label(code, numerator, denominator) -> str: - """Read-back for the headless engine: the code-75 label, else the custom ratio.""" - return _DXF_SCALE_LABEL.get(int(code)) or scale_label(numerator, denominator) + """Read-back for the headless engine: the code-75 label (``fit`` for 0, + the ratio ``DXF_STD_SCALES`` gives the code, an AutoCAD-style name for an + imperial code), else the custom ratio.""" + label = _DXF_SCALE_LABEL.get(int(code)) + if label is not None: + return label + return scale_label(numerator, denominator) def activex_scale_label(code) -> str: diff --git a/tests/test_page_setup.py b/tests/test_page_setup.py index 6491bbc..a3ec5ab 100644 --- a/tests/test_page_setup.py +++ b/tests/test_page_setup.py @@ -13,10 +13,14 @@ import zlib import pytest from engineering.standards.papers import ( + ACTIVEX_PLOT_SCALE, CTB_CATALOG, + DXF_STANDARD_SCALE_TYPE, + DXF_STD_SCALES, PAPER_SIZES, SCALES, canonical_media_name, + dxf_scale_label, paper_from_size, paper_units_name, resolve_page_setup, @@ -110,19 +114,58 @@ def test_resolve_page_setup_accepts_lowercase_and_short_paper_names(): def test_resolve_page_setup_scale_codes(): one_to_five = resolve_page_setup("ISO_A3", scale="1:5") - assert one_to_five["dxf_standard_scale_type"] is None # DXF code 75 has no 1:5 - # The typelib declares ac1_5 = 19, but AutoCAD 2026 refuses - # StandardScale = 19 ("Invalid input", measured live): custom route. + # Code 19 is 1:5 in the file enum, but the typelib's ac1_5 = 19 is refused + # by AutoCAD 2026 ("Invalid input", measured live), so both engines store + # 1:5 the same way: the custom route, 142/143 = 1/5. + assert one_to_five["dxf_standard_scale_type"] is None assert one_to_five["activex_standard_scale"] is None assert one_to_five["scale_ratio"] == [1.0, 5.0] five_to_one = resolve_page_setup("ISO_A3", scale="5:1") assert five_to_one["dxf_standard_scale_type"] is None assert five_to_one["activex_standard_scale"] is None assert five_to_one["scale_ratio"] == [5.0, 1.0] - assert resolve_page_setup("ISO_A3", scale="1:50")["dxf_standard_scale_type"] == 25 + # One numbering: AutoCAD 2026 saves StandardScale = 26 (ac1_50) as + # group 75 = 26 (measured, COM SaveAs DXF) — not ezdxf's 25. + assert resolve_page_setup("ISO_A3", scale="1:50")["dxf_standard_scale_type"] == 26 assert resolve_page_setup("ISO_A3", scale="1:50")["activex_standard_scale"] == 26 +def test_dxf_standard_scale_codes_are_the_enum_autocad_writes(): + """Group 75 is the ObjectARX ``StdScaleType`` enum, the same numbering as + ActiveX ``AcPlotScale`` — measured on AutoCAD 2026 (``StandardScale = n`` + then ``SaveAs(..., 61)``): 1:2→17, 1:10→21, 1:50→26, 2:1→28, 10:1→31. + ezdxf's ``STD_SCALES`` has no 19 = 1:5 and is one lower above 18; read + through it, an AutoCAD 1:50 sheet is "1:100" and a 1:10 sheet plots 1:16. + """ + measured = {"fit": 0, "1:1": 16, "1:2": 17, "1:10": 21, "1:50": 26, "2:1": 28, "10:1": 31} + for label, code in measured.items(): + assert DXF_STANDARD_SCALE_TYPE[label] == code, label + # Every code the writer uses is the ActiveX code for the same label … + for label, code in DXF_STANDARD_SCALE_TYPE.items(): + assert ACTIVEX_PLOT_SCALE[label] == code, label + # … and its authored ratio is the catalogue factor. + for label, code in DXF_STANDARD_SCALE_TYPE.items(): + if code: + numerator, denominator = DXF_STD_SCALES[code] + assert numerator / denominator == pytest.approx(SCALES[label]), label + # The full file enum, including what the writer never emits. + assert DXF_STD_SCALES[19] == (1.0, 5.0) + assert DXF_STD_SCALES[20] == (1.0, 8.0) + assert DXF_STD_SCALES[22] == (1.0, 16.0) + assert DXF_STD_SCALES[25] == (1.0, 40.0) + assert DXF_STD_SCALES[33] == (1000.0, 1.0) + assert set(DXF_STD_SCALES) == set(range(1, 34)) + # Read-back labels come from the code, never from a stale 142/143. + assert dxf_scale_label(0, 3.0, 7.0) == "fit" + assert dxf_scale_label(26, 1.0, 100.0) == "1:50" + assert dxf_scale_label(21, 1.0, 1.0) == "1:10" + assert dxf_scale_label(19, 1.0, 1.0) == "1:5" + assert dxf_scale_label(22, 1.0, 1.0) == "1:16" + assert dxf_scale_label(28, 1.0, 0.5) == "2:1" + assert dxf_scale_label(8, 1.0, 1.0) == "1/4\"=1'" + assert dxf_scale_label(99, 1.0, 3.0) == "1:3" # unknown code: the stored ratio + + @pytest.mark.parametrize( "kwargs, field", [ @@ -392,7 +435,8 @@ async def test_layout_plot_honours_the_plot_scale(backend, tmp_path): async def test_custom_scale_sizes_the_window_too(backend): - """1:5 has no DXF code 75, so it rides on the numerator / denominator.""" + """1:5 takes the custom route (as on the live engine), 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)) @@ -526,9 +570,103 @@ async def test_custom_scale_clears_the_standard_scale_bit_and_writes_the_ratio(b dxf = backend._doc.layouts.get(SHEET).dxf_layout.dxf assert not int(dxf.plot_layout_flags) & 16 assert (float(dxf.scale_numerator), float(dxf.scale_denominator)) == (1.0, 5.0) + assert float(dxf.unit_factor) == pytest.approx(0.2) assert (await backend.page_setup_list(SHEET))[0]["scale"] == "1:5" +@pytest.mark.parametrize( + "label, code, factor", + [ + ("fit", 0, 1.0), + ("1:1", 16, 1.0), + ("1:2", 17, 0.5), + ("1:10", 21, 0.1), + ("1:20", 23, 0.05), + ("1:50", 26, 0.02), + ("1:100", 27, 0.01), + ("2:1", 28, 2.0), + ("10:1", 31, 10.0), + ], +) +async def test_standard_scale_writes_the_code_and_the_factor_autocad_plots_by( + backend, label, code, factor +): + """Under USE_STANDARD_SCALE AutoCAD's plot engine scales by group 147 + alone (measured, AutoCAD 2026: a headless 1:10 sheet with code 21 and + 147 = 1.0 plotted a 100-unit line at 99.99 mm — 1:1 — while the row, + ``applied`` and 142/143 all said 1:10; patching 147 to 0.1 alone gave + 9.99 mm). AutoCAD writes 147 = numerator / denominator, 1.0 for fit; so + does the headless writer, on every route, with the code AutoCAD uses. + """ + await _sheet(backend) + result = await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale=label)) + assert result["ok"] is True and result["applied"]["scale"] == label + dxf = backend._doc.layouts.get(SHEET).dxf_layout.dxf + assert int(dxf.plot_layout_flags) & 16 + assert int(dxf.standard_scale_type) == code + assert float(dxf.unit_factor) == pytest.approx(factor) + ratio = [1.0, 1.0] if label == "fit" else list(scale_ratio(label)) + assert [float(dxf.scale_numerator), float(dxf.scale_denominator)] == ratio + assert (await backend.page_setup_list(SHEET))[0]["scale"] == label + frame = backend._paper_frame(backend._doc.layouts.get(SHEET)) + assert frame["scale"] == label and frame["scale_applied"] is True + if label != "fit": + assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(420.0 / factor) + + +def _autocad_authored_scale(layout, code, numerator, denominator, factor): + """Store the plot scale exactly as AutoCAD 2026 saves it (measured via COM + ``SaveAs(..., 61)``): group 75 from the ObjectARX enum, 142/143 as the + ratio (AutoCAD normalises 2:1 to 1 / 0.5), 147 as the factor, bit 16 set. + """ + dxf = layout.dxf_layout.dxf + if not dxf.hasattr("plot_layout_flags"): + dxf.plot_layout_flags = dxf.get_default("plot_layout_flags") + dxf.paper_size = "ISO_A3_(420.00_x_297.00_MM)" + dxf.paper_width, dxf.paper_height = 420.0, 297.0 + dxf.plot_paper_units = 1 + dxf.plot_rotation = 0 + dxf.standard_scale_type = code + dxf.scale_numerator, dxf.scale_denominator = numerator, denominator + dxf.unit_factor = factor + layout.use_standard_scale(True) + layout.set_plot_type(5) + + +@pytest.mark.parametrize( + "code, numerator, denominator, factor, label, units_across", + [ + (21, 1.0, 10.0, 0.1, "1:10", 4200.0), + (23, 1.0, 20.0, 0.05, "1:20", 8400.0), + (26, 1.0, 50.0, 0.02, "1:50", 21000.0), + (27, 1.0, 100.0, 0.01, "1:100", 42000.0), + (28, 1.0, 0.5, 2.0, "2:1", 210.0), + (31, 1.0, 0.1, 10.0, "10:1", 42.0), + (19, 1.0, 5.0, 0.2, "1:5", 2100.0), + (22, 1.0, 16.0, 0.0625, "1:16", 6720.0), + ], +) +async def test_list_and_render_read_an_autocad_authored_sheet_at_its_own_scale( + backend, code, numerator, denominator, factor, label, units_across +): + """A sheet AutoCAD saved at 1:50 (group 75 = 26) reads back as 1:50 and + renders 21000 paper-space units across the 420 mm — not "1:100" / 42000 + (what ezdxf's one-off ``STD_SCALES`` made of it: 1:10 rendered at 1:16, + 1:20 at 1:30, 1:50 at 1:100, 1:100 at 2:1, 2:1 at 4:1, 10:1 at 100:1, + with the row and the plot disagreeing). Codes 19 and 22 are ones this + server never writes, so this reads the file enum, not the writer's table. + """ + await _sheet(backend) + lay = backend._doc.layouts.get(SHEET) + _autocad_authored_scale(lay, code, numerator, denominator, factor) + row = (await backend.page_setup_list(SHEET))[0] + assert row["scale"] == label + frame = backend._paper_frame(lay) + assert frame["scale"] == label and frame["effective_scale"] == label + assert frame["scale_applied"] is True + assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(units_across) + + async def test_apply_keeps_the_viewports(backend): await _sheet(backend) await backend.viewport_create(SHEET, 210, 148.5, 260, 180, 0, 0, scale=1.0)