fix(task-17): the headless page setup writes the scale AutoCAD plots by, and reads the code 75 AutoCAD writes
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
f2386a9fdd
commit
aeb27c252b
+18
-13
@@ -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.
|
||||
|
||||
+120
-21
@@ -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:
|
||||
|
||||
+143
-5
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user