diff --git a/docs/superpowers/plans/2026-09-16-v1.6-settings-implementation.md b/docs/superpowers/plans/2026-09-16-v1.6-settings-implementation.md index cb2b59e..9de3233 100644 --- a/docs/superpowers/plans/2026-09-16-v1.6-settings-implementation.md +++ b/docs/superpowers/plans/2026-09-16-v1.6-settings-implementation.md @@ -3351,7 +3351,7 @@ Part of `docs/superpowers/plans/2026-09-16-v1.6-settings-implementation.md`. Its - Test: `tests/test_standards_presets.py` **Interfaces:** -- Produces: `PRESETS = {"iso-25": {...17 vars...}, "ansi": {...}}`, `PRESET_VARIABLES` (the 17, spec order), `DIM_VARIABLE_WHITELIST: frozenset` (17 + 16 = 33), `DIM_VARIABLE_RANGES: dict[str, tuple | set]`, `resolve_dimstyle(preset, overrides) -> dict`, `validate_overrides(overrides) -> dict`, `check_dim_value(name, value)`, `describe_preset(name) -> dict`; `TEXT_PRESETS`, `resolve_font(font) -> (font_file, known)`, `validate_textstyle(name, font, height, width_factor, oblique_deg) -> dict`; `MLEADER_PRESETS`, `MLEADER_KEYS`, `resolve_mleaderstyle(preset, overrides) -> dict`. +- Produces: `PRESETS = {"iso-25": {...17 vars...}, "ansi": {...}}`, `PRESET_VARIABLES` (the 17, spec order), `DIM_VARIABLE_WHITELIST: frozenset` (17 + 16 = 33), `DIM_VARIABLE_RANGES: dict[str, tuple | set]`, `resolve_dimstyle(preset, overrides) -> dict`, `validate_overrides(overrides) -> dict`, `check_dim_value(name, value)`, `describe_preset(name) -> dict`; the arrowhead vocabulary (added by the Task 9 review fixes and consumed by Task 10's helpers below): `ARROWHEAD_BLOCKS: frozenset` (AutoCAD's nineteen named built-ins, `_OBLIQUE` spelling, pinned equal to ezdxf's `ARROWS.__acad__`), `canonical_arrowhead(key, value) -> str` (what `check_dim_value` returns for `DIMBLK`/`DIMBLK1`/`DIMBLK2`: `""` for closed filled and its aliases `.`/`CLOSEDFILLED`/`_CLOSEDFILLED`, a built-in in any case with or without its underscore → its block name, anything else a user block name under the symbol-name rule — whether that block *exists* is the backend's check against `doc.blocks`, before any write), `ezdxf_arrowhead(canonical) -> str` (the one translation the headless **writer** needs: `_OBLIQUE` → `OBLIQUE`, user blocks and `""` unchanged — ezdxf raises `DXFValueError: Block "_OBLIQUE" does not exist` at `doc.write` and `DXFUndefinedBlockError` at render if the canonical spelling is stored verbatim), `reported_arrowhead(raw) -> str` (the **read** side, never raises: ezdxf reports `OBLIQUE` in-session and `_OBLIQUE` after a reload, COM reports `_OBLIQUE`; both come back as the canonical name, a foreign name passes through); `TEXT_PRESETS`, `resolve_font(font) -> (font_file, known)`, `validate_textstyle(name, font, height, width_factor, oblique_deg) -> dict`; `MLEADER_PRESETS`, `MLEADER_KEYS`, `resolve_mleaderstyle(preset, overrides) -> dict`. - Every refusal is `ValueError` (wrong type: `TypeError`) naming the key, and nothing in this package writes anything — the backends call these before they touch a table. - Groups C and P add `sysvars.py`, `papers.py`, `pdfinfo.py`, `templates.py`, `plot.py` to the same package and import them by path; this `__init__.py` re-exports only group S's names, so those groups never need to edit it. @@ -3821,7 +3821,9 @@ LINEWEIGHT_CODES: frozenset[int] = frozenset(int(code) for code in _LINEWEIGHT_T #: * ``("int", low, high)`` — an integer (a float with no fraction is accepted). #: * ``("nonzero_float", low, high)`` — as ``float`` but ``0`` is refused. #: * ``("char",)`` — exactly one character. -#: * ``("str",)`` — any string (arrowhead block names; ``""`` is the closed filled arrow). +#: * ``("arrow",)`` — an arrowhead: ``""`` (closed filled), one of ``ARROWHEAD_BLOCKS`` +#: given with or without its underscore in any case, or a user block name under the +#: symbol-name rule; ``check_dim_value`` returns ``canonical_arrowhead(key, value)``. #: * ``("name",)`` — a non-empty DXF symbol-table name. #: * a ``set`` — one of its members. DIM_VARIABLE_RANGES: dict[str, tuple | set] = { @@ -3838,15 +3840,20 @@ DIM_VARIABLE_RANGES: dict[str, tuple | set] = { "DIMDSEP": ("char",), "DIMLUNIT": ("int", 1, 6), "DIMZIN": ("int", 0, 15), # a bitmask - "DIMBLK": ("str",), + "DIMBLK": ("arrow",), "DIMTXSTY": ("name",), "DIMLWD": LINEWEIGHT_CODES, "DIMLWE": LINEWEIGHT_CODES, "DIMSCALE": ("float", 0.0, 1e6), # 0 means "scale to the viewport" # tolerances, rounding, fit, colours, arrowheads — the rest of the whitelist "DIMTOL": {0, 1}, - "DIMTP": ("float", 0.0, 1e6), - "DIMTM": ("float", 0.0, 1e6), + # Both signed, as AutoCAD documents them: the displayed lower deviation is + # -DIMTM, so a double-positive fit (ISO 286 p6, +0.035/+0.022) stores + # DIMTM -0.022 and a double-negative one (f7, -0.020/-0.041) stores DIMTP + # -0.020 — exactly what engineering/tolerances.py::build_dim_override emits + # per dimension; a style must be able to hold the same numbers. + "DIMTP": ("float", -1e6, 1e6), + "DIMTM": ("float", -1e6, 1e6), "DIMTOLJ": ("int", 0, 2), "DIMTFAC": ("float", 1e-9, 1e6), "DIMLFAC": ("nonzero_float", -1e6, 1e6), @@ -3857,8 +3864,8 @@ DIM_VARIABLE_RANGES: dict[str, tuple | set] = { "DIMCLRE": ("int", 0, 256), "DIMCLRT": ("int", 0, 256), "DIMSAH": {0, 1}, - "DIMBLK1": ("str",), - "DIMBLK2": ("str",), + "DIMBLK1": ("arrow",), + "DIMBLK2": ("arrow",), "DIMCEN": ("float", -1e6, 1e6), # negative draws centre lines, 0 none } @@ -4224,7 +4231,7 @@ git commit -m "feat(standards): ISO-25 and ANSI dimension, text and leader style - Test: `tests/test_styles.py` **Interfaces:** -- Consumes: `resolve_dimstyle`, `validate_overrides`, `PRESET_VARIABLES`, `DIM_VARIABLE_WHITELIST`, `TEXT_PRESETS`, `validate_textstyle`, `MLEADER_KEYS` (Task 9); `sanitize_symbol_name` (`security.py`) — note it raises `ToolError`, not `ValueError`, on an empty or illegal style name, the same refusal the existing block and layer tools give (the skeleton's Global Constraints record this as the one standing exception to the `ValueError`/`TypeError` rule); `_async`/`_require_doc`/`_mark_dirty` (ezdxf), `_run`/`_acad_doc`/`_acad_app`/`_regen`/`deg2rad`/`rad2deg` (COM). +- Consumes: `resolve_dimstyle`, `validate_overrides`, `PRESET_VARIABLES`, `DIM_VARIABLE_WHITELIST`, `ARROWHEAD_BLOCKS`, `ezdxf_arrowhead`, `reported_arrowhead`, `TEXT_PRESETS`, `validate_textstyle`, `MLEADER_KEYS` (Task 9) — `validate_overrides` already returns `DIMBLK`/`DIMBLK1`/`DIMBLK2` in the canonical `_OBLIQUE` spelling, which ActiveX takes verbatim but ezdxf's writer refuses, so the headless helpers translate on write (`ezdxf_arrowhead`), canonicalise on read (`reported_arrowhead`, both engines) and refuse a user block absent from `doc.blocks` before any write; `sanitize_symbol_name` (`security.py`) — note it raises `ToolError`, not `ValueError`, on an empty or illegal style name, the same refusal the existing block and layer tools give (the skeleton's Global Constraints record this as the one standing exception to the `ValueError`/`TypeError` rule); `_async`/`_require_doc`/`_mark_dirty` (ezdxf), `_run`/`_acad_doc`/`_acad_app`/`_regen`/`deg2rad`/`rad2deg` (COM). - Produces: the nine `StylesContract` methods on both backends, exactly the plan's signatures. Extra keys beyond the plan's row shapes, on both engines: `dimstyle_list` rows carry `values_available: bool`; `dimstyle_create` carries `written: [vars]`; `dimstyle_set_current` / `textstyle_set_current` carry `changed: bool`; `mleaderstyle_create` carries `textstyle_created`. Names are matched case-insensitively (AutoCAD's rule) and reported in the table's own spelling. - The COM rule, stated once in the code and enforced by the fake: `AcadDimStyle` has `Name` and `CopyFrom` only. Values are written by setting the `DIM*` document variables and `CopyFrom(document)`; making a style current (`ActiveDimStyle`) restores its saved settings and drops unsaved overrides; reading a non-current style's values is impossible, so `dimstyle_list` reports `values` for the current style only. `mleaderstyle_create` stays the `@capability("mleaderstyle")` default on COM (never defined there); `mleaderstyle_list` reads names from the `ACAD_MLEADERSTYLE` dictionary with `values_available: false`. @@ -4368,6 +4375,46 @@ async def test_refusals_happen_before_anything_is_written(backend): assert backend._doc.dimstyles.get("ISO-25").dxf.dimdec == 2 +async def test_builtin_arrowheads_render_save_and_report_one_spelling(backend, tmp_path): + """`validate_overrides` hands the canonical `_OBLIQUE`; stored verbatim ezdxf + raises at render and at `doc.write`. The witness is the rendered INSERT and + a save/reopen that reports the same name before and after.""" + values = resolve_dimstyle("iso-25", {"DIMBLK": "oblique", "DIMSAH": 1, "DIMBLK1": "_DOT", + "DIMBLK2": "."}) + result = await backend.dimstyle_create("ISO-25", values, set_current=True) + assert result["values"]["DIMBLK"] == "_OBLIQUE" + style = backend._doc.dimstyles.get("ISO-25") + assert style.dxf.dimblk == "OBLIQUE" and style.dxf.dimblk1 == "DOT" and style.dxf.dimblk2 == "" + dim = await backend.dimension_linear(0, 0, 20, 0, 10, -10) + block = backend._doc.blocks.get(backend._doc.entitydb.get(dim.handle).dxf.geometry) + assert {e.dxf.name for e in block if e.dxftype() == "INSERT"} == {"_DOT", "_CLOSEDFILLED"}, ( + "DIMSAH on: the two separate arrowheads win over DIMBLK" + ) + again = await backend.dimstyle_modify("ISO-25", {"DIMBLK": "OBLIQUE", "DIMBLK1": "_dot"}) + assert again["changed"] == {}, "re-setting a value to itself is not a change (spec §8.3)" + path = str(tmp_path / "arrows.dxf") + await backend.drawing_save_as(path) + await backend.drawing_open(path) + rows = {r["name"]: r for r in await backend.dimstyle_list()} + assert rows["ISO-25"]["values"]["DIMBLK"] == "_OBLIQUE", "reloaded, the same spelling" + reopened = await backend.dimstyle_modify("ISO-25", {"DIMBLK": "oblique"}) + assert reopened["changed"] == {} + + +async def test_user_arrowhead_block_must_exist_before_anything_is_written(backend): + with pytest.raises(ValueError, match="DIMBLK: block 'MYARROW' does not exist"): + await backend.dimstyle_create("X", resolve_dimstyle("iso-25", {"DIMBLK": "MYARROW"})) + assert not backend._doc.dimstyles.has_entry("X") + await backend.dimstyle_create("ISO-25", resolve_dimstyle("iso-25", None)) + with pytest.raises(ValueError, match="DIMBLK1: block 'MYARROW' does not exist"): + await backend.dimstyle_modify("ISO-25", {"DIMSAH": 1, "DIMBLK1": "MYARROW"}) + assert not backend._doc.dimstyles.get("ISO-25").dxf.hasattr("dimsah"), "nothing written" + backend._doc.blocks.new("MYARROW").add_line((0, 0), (1, 1)) + result = await backend.dimstyle_modify("ISO-25", {"DIMBLK": "myarrow"}) + assert result["changed"] == {"DIMBLK": ["", "MYARROW"]}, "the block table's own spelling" + assert backend._doc.dimstyles.get("ISO-25").dxf.dimblk == "MYARROW" + + async def test_dimstyle_set_current_reports_previous_and_survives_save_reopen(backend, tmp_path): await backend.dimstyle_create("ISO-25", resolve_dimstyle("iso-25", None)) result = await backend.dimstyle_set_current("iso-25") @@ -5019,8 +5066,14 @@ def _dimstyle_schema_default(var: str): return None if attrib is None else attrib.default +_ARROWHEAD_VARS = ("DIMBLK", "DIMBLK1", "DIMBLK2") + + def _dimstyle_value(style, var: str): - """A style's value for ``var`` in the tool vocabulary (DIMDSEP as the character).""" + """A style's value for ``var`` in the tool vocabulary (DIMDSEP as the + character, arrowheads in the canonical ``_OBLIQUE`` spelling).""" + from engineering.standards.dimstyles import reported_arrowhead + attr = var.lower() raw = style.dxf.get(attr) if style.dxf.hasattr(attr) else None if raw is None: @@ -5030,6 +5083,12 @@ def _dimstyle_value(style, var: str): return "," if code == 0 else chr(code) if var == "DIMTXSTY": return str(raw) if raw else "Standard" + if var in _ARROWHEAD_VARS: + # Measured: ezdxf holds the writer's ``OBLIQUE`` in-session and the + # block name ``_OBLIQUE`` after a reload. Raw, one style would be + # spelled two ways and re-setting a value to itself would report a + # change (spec §8.3); canonicalised, both reads agree. + return reported_arrowhead(raw) return raw @@ -5039,19 +5098,58 @@ def _read_dimstyle_values(style, variables=None) -> dict[str, Any]: return {var: _dimstyle_value(style, var) for var in (variables or PRESET_VARIABLES)} +def _require_arrowhead_blocks(doc, values: dict[str, Any]) -> None: + """Refuse a user arrowhead block the drawing does not define — before any write. + + A built-in (``ARROWHEAD_BLOCKS``) and closed filled (``""``) need no block: + ezdxf creates the built-in's block on first use. A user block name passes + the data layer (it cannot see the drawing) and, stored, ezdxf raises + ``DXFUndefinedBlockError`` at the next dimension render and ``DXFValueError: + Block "MYARROW" does not exist`` at ``doc.write`` — after the create tool + already said ``ok``. ``doc.blocks`` membership is case-insensitive, and a + reload reports the block table's own spelling (measured: ``myarrow`` stored, + ``MYARROW`` after reopen), so the name is rewritten in ``values`` to that + spelling here — the same "reported in the table's own spelling" rule the + style names follow, and what keeps a re-set after reopen a non-change. + """ + from engineering.standards.dimstyles import ARROWHEAD_BLOCKS + + for var in _ARROWHEAD_VARS: + name = values.get(var) + if not name or name in ARROWHEAD_BLOCKS: + continue + if name not in doc.blocks: + raise ValueError( + f"{var}: block {name!r} does not exist in this drawing; define it first " + "with block_define, or name one of AutoCAD's built-in arrowheads" + ) + values[var] = doc.blocks.get(name).name + + def _write_dimstyle_values(style, values: dict[str, Any]) -> None: """Store typed values (from ``validate_overrides``) on a DIMSTYLE entry. DIMRND 0 is AutoCAD's "no rounding"; ezdxf applies ``xround(x, 0.0)`` to a *stored* 0.0 and rounds every dimension to a whole unit, so it is discarded rather than stored (the `_normalise_dimstyle_rounding` lesson). + + Arrowheads arrive in the canonical ``_OBLIQUE`` spelling (what ActiveX + takes verbatim); ezdxf's writer wants ``OBLIQUE`` and raises at + ``doc.write`` on the underscore form, so they go through + ``ezdxf_arrowhead``. The caller has run ``_require_arrowhead_blocks``. """ + from engineering.standards.dimstyles import ezdxf_arrowhead + for var, value in values.items(): attr = var.lower() if var == "DIMRND" and float(value) == 0.0: style.dxf.discard("dimrnd") continue - style.dxf.set(attr, ord(value) if var == "DIMDSEP" else value) + if var == "DIMDSEP": + value = ord(value) + elif var in _ARROWHEAD_VARS: + value = ezdxf_arrowhead(value) + style.dxf.set(attr, value) def _sync_header_to_dimstyle(doc, style) -> None: @@ -5179,6 +5277,7 @@ Insert immediately before ` # ── corner ops` in `EzdxfBackend` (directly "use dimstyle_modify to change it" ) previous = _current_dimstyle_name(doc) + _require_arrowhead_blocks(doc, typed) txsty, textstyle_created = _ensure_textstyle( doc, str(typed.get("DIMTXSTY", "Standard")), refusal_key="DIMTXSTY" ) @@ -5225,6 +5324,7 @@ Insert immediately before ` # ── corner ops` in `EzdxfBackend` (directly f"DIMTXSTY: text style {typed['DIMTXSTY']!r} does not exist in this drawing; " "create it first with textstyle_create" ) + _require_arrowhead_blocks(doc, typed) before = _read_dimstyle_values(style, tuple(typed)) _write_dimstyle_values(style, typed) after = _read_dimstyle_values(style, tuple(typed)) @@ -5458,7 +5558,14 @@ def _com_dimvar_for_report(var: str, raw: Any) -> Any: return "," if raw == 0 else chr(raw) text = str(raw) return text[:1] if text else "," - if var in ("DIMBLK", "DIMBLK1", "DIMBLK2", "DIMTXSTY"): + if var in ("DIMBLK", "DIMBLK1", "DIMBLK2"): + # AutoCAD reports the block name (``_OBLIQUE``, ``""`` for closed + # filled) — already canonical; the same read-side rule as ezdxf keeps + # the two engines' rows spelled identically. + from engineering.standards.dimstyles import reported_arrowhead + + return reported_arrowhead(raw) + if var == "DIMTXSTY": return str(raw) return raw diff --git a/engineering/standards/__init__.py b/engineering/standards/__init__.py index 90b4f1a..abb8477 100644 --- a/engineering/standards/__init__.py +++ b/engineering/standards/__init__.py @@ -19,6 +19,7 @@ from engineering.standards.dimstyles import ( check_dim_value, describe_preset, ezdxf_arrowhead, + reported_arrowhead, resolve_dimstyle, validate_overrides, ) @@ -37,6 +38,7 @@ __all__ = [ "check_dim_value", "describe_preset", "ezdxf_arrowhead", + "reported_arrowhead", "resolve_dimstyle", "resolve_font", "resolve_mleaderstyle", diff --git a/engineering/standards/dimstyles.py b/engineering/standards/dimstyles.py index e6b5f1a..f7ce151 100644 --- a/engineering/standards/dimstyles.py +++ b/engineering/standards/dimstyles.py @@ -41,6 +41,7 @@ __all__ = [ "check_dim_value", "describe_preset", "ezdxf_arrowhead", + "reported_arrowhead", "resolve_dimstyle", "validate_overrides", ] @@ -179,8 +180,13 @@ DIM_VARIABLE_RANGES: dict[str, tuple | set] = { "DIMSCALE": ("float", 0.0, 1e6), # 0 means "scale to the viewport" # tolerances, rounding, fit, colours, arrowheads — the rest of the whitelist "DIMTOL": {0, 1}, - "DIMTP": ("float", 0.0, 1e6), - "DIMTM": ("float", 0.0, 1e6), + # Both signed, as AutoCAD documents them: the displayed lower deviation is + # -DIMTM, so a double-positive fit (ISO 286 p6, +0.035/+0.022) stores + # DIMTM -0.022 and a double-negative one (f7, -0.020/-0.041) stores DIMTP + # -0.020 — exactly what engineering/tolerances.py::build_dim_override emits + # per dimension; a style must be able to hold the same numbers. + "DIMTP": ("float", -1e6, 1e6), + "DIMTM": ("float", -1e6, 1e6), "DIMTOLJ": ("int", 0, 2), "DIMTFAC": ("float", 1e-9, 1e6), "DIMLFAC": ("nonzero_float", -1e6, 1e6), @@ -229,6 +235,27 @@ def canonical_arrowhead(key: str, value: Any) -> str: return check_name(key, text, what="block name") +def reported_arrowhead(raw: Any) -> str: + """A *stored* arrowhead name in the canonical spelling, never raising. + + The read-side twin of :func:`canonical_arrowhead`: ezdxf reports the name + the writer stored (``OBLIQUE``) in-session and the block name + (``_OBLIQUE``) after a reload, so a raw read would spell one style two + ways and make re-setting a value to itself look like a change (spec §8.3). + A built-in in any spelling comes back as its block name and the closed + filled aliases as ``""``; ``None`` is ``""``; anything else — a user block, + or a foreign drawing's name the symbol-name rule would refuse — passes + through unchanged, because a read must not raise. + """ + if raw is None: + return "" + text = str(raw) + try: + return canonical_arrowhead("DIMBLK", text) + except (TypeError, ValueError): + return text + + def ezdxf_arrowhead(canonical: str) -> str: """The name ezdxf's DIMSTYLE takes for a :func:`canonical_arrowhead` value. diff --git a/tests/test_standards_presets.py b/tests/test_standards_presets.py index cacd97f..61def1a 100644 --- a/tests/test_standards_presets.py +++ b/tests/test_standards_presets.py @@ -22,6 +22,7 @@ from engineering.standards.dimstyles import ( check_dim_value, describe_preset, ezdxf_arrowhead, + reported_arrowhead, resolve_dimstyle, validate_overrides, ) @@ -206,6 +207,51 @@ def test_validate_overrides_alone_is_what_modify_uses(): assert check_dim_value("DIMBLK", "_OBLIQUE") == "_OBLIQUE" +# ── tolerances: DIMTP / DIMTM are signed ──────────────────────────────────── + + +@pytest.mark.parametrize( + ("fit", "expected"), + [ + ("p6", {"DIMTP": 0.035, "DIMTM": -0.022}), # double positive: -DIMTM is +0.022 + ("f7", {"DIMTP": -0.02, "DIMTM": 0.041}), # double negative: DIMTP below zero + ("H7", {"DIMTP": 0.021, "DIMTM": 0.0}), + ("h6", {"DIMTP": 0.0, "DIMTM": 0.013}), + ], +) +def test_a_style_holds_the_same_signed_deviations_a_dimension_gets(fit, expected): + """``dimension_linear(fit=...)`` writes ``build_dim_override``'s DIMTP/DIMTM + onto one dimension; a dimension style must accept the identical numbers, + signs included, or a p6 / f7 fit can be drawn but never styled — and an + agent obeying an "at least 0" refusal would ship a sign-flipped tolerance.""" + from engineering.fits import fit_lookup + from engineering.tolerances import build_dim_override + + deviation = fit_lookup(fit, 20.0) + override, _ = build_dim_override(deviation.upper_mm, -deviation.lower_mm, "deviation", None) + styled = {k: v for k, v in override.items() if k.upper() in DIM_VARIABLE_WHITELIST} + assert {"dimtol", "dimtp", "dimtm"} <= set(styled) + typed = resolve_dimstyle("iso-25", styled) + assert typed["DIMTOL"] == 1 + assert typed["DIMTP"] == pytest.approx(expected["DIMTP"]) + assert typed["DIMTM"] == pytest.approx(expected["DIMTM"]) + assert typed["DIMTM"] == pytest.approx(override["dimtm"]), "sign preserved, not abs()" + assert typed["DIMTP"] == pytest.approx(override["dimtp"]) + + +@pytest.mark.parametrize("var", ["DIMTP", "DIMTM"]) +def test_tolerance_limits_are_signed_finite_floats(var): + assert DIM_VARIABLE_RANGES[var] == ("float", -1e6, 1e6) + assert check_dim_value(var, -0.022) == -0.022 + assert check_dim_value(var, 0) == 0.0 + with pytest.raises(ValueError, match=var): + check_dim_value(var, float("-inf")) + with pytest.raises(ValueError, match=var): + check_dim_value(var, -1e7) + with pytest.raises(TypeError, match=var): + check_dim_value(var, "-0.022") + + # ── arrowheads: DIMBLK / DIMBLK1 / DIMBLK2 ────────────────────────────────── @@ -298,6 +344,47 @@ def test_the_canonical_spelling_is_not_what_the_headless_writer_takes(): doc.write(io.StringIO()) +@pytest.mark.parametrize( + ("stored", "reported"), + [ + ("OBLIQUE", "_OBLIQUE"), # what ezdxf holds in-session after the writer set it + ("_OBLIQUE", "_OBLIQUE"), # what ezdxf holds after a reload; what COM reports + ("", ""), + (None, ""), + ("_CLOSEDFILLED", ""), + ("MYARROW", "MYARROW"), + ("MY ARROW,1", "MY ARROW,1"), # a foreign drawing's name: passed through, never raised + (" ", " "), + ], +) +def test_reported_arrowhead_spells_a_stored_name_one_way_and_never_raises(stored, reported): + assert reported_arrowhead(stored) == reported + + +@pytest.mark.parametrize("canonical", ["_OBLIQUE", "_DOT", "", "MYARROW"]) +def test_a_style_reports_the_same_arrowhead_before_and_after_a_reload(canonical): + """The spec §8.3 witness at the data layer: a raw read spells ``OBLIQUE`` + in-session and ``_OBLIQUE`` after reopen, so ``dimstyle_modify`` re-setting + the same value would report a change. Through ``reported_arrowhead`` the two + reads agree with each other and with what ``check_dim_value`` accepts.""" + import io + + import ezdxf + + doc = ezdxf.new("R2018") + if canonical == "MYARROW": + doc.blocks.new("MYARROW").add_line((0, 0), (1, 1)) + style = doc.dimstyles.new("X") + style.dxf.dimblk = ezdxf_arrowhead(canonical) + in_session = reported_arrowhead(style.dxf.dimblk) + buffer = io.StringIO() + doc.write(buffer) + buffer.seek(0) + reloaded = reported_arrowhead(ezdxf.read(buffer).dimstyles.get("X").dxf.dimblk) + assert in_session == reloaded == canonical + assert check_dim_value("DIMBLK", in_session) == canonical + + # ── the symbol-name rule ────────────────────────────────────────────────────