fix(task-9): signed DIMTP/DIMTM, read-side arrowhead spelling, and the Task 10 handshake

Two blocking review findings on the standards data layer:

* DIMTP and DIMTM were pinned to ("float", 0.0, 1e6), refusing every signed
  deviation with a false "must be at least 0". AutoCAD documents both as
  signed and the repo already relies on it: build_dim_override writes
  dimtm=-0.022 for ISO 286 p6 and dimtp=-0.020 for f7, and
  dimension_linear(fit=...) emits exactly those. A dimension style could
  therefore never hold a double-positive or double-negative fit that the same
  server draws correctly per dimension, and an agent obeying the message
  would ship a sign-flipped tolerance. Both are now ("float", -1e6, 1e6); a
  provenance test resolves the p6/f7/H7/h6 overrides build_dim_override
  emits through resolve_dimstyle and pins the signs.

* 827719f made check_dim_value return DIMBLK/DIMBLK1/DIMBLK2 in the
  canonical _OBLIQUE spelling and added ezdxf_arrowhead without telling
  Task 10, whose plan helpers store the value verbatim and read the raw
  string back - executed as written the headless drawing raised at render
  and at doc.write, a user block passed the data layer and failed at save,
  and ezdxf's OBLIQUE-in-session / _OBLIQUE-after-reload split made an
  idempotent re-set report a change (spec section 8.3). This commit adds the
  read-side twin reported_arrowhead (never raises; a foreign name passes
  through), pins it with an in-session-equals-reloaded round trip, and edits
  the plan: Task 9 Produces and Task 10 Consumes now name ARROWHEAD_BLOCKS,
  canonical_arrowhead, ezdxf_arrowhead and reported_arrowhead; Task 10's
  _write_dimstyle_values maps through ezdxf_arrowhead, _dimstyle_value and
  _com_dimvar_for_report map through reported_arrowhead, a new
  _require_arrowhead_blocks refuses a user block absent from doc.blocks
  before dimstyle_create/modify write anything (and rewrites the name to the
  block table's own spelling, measured to be what a reload reports), and two
  headless tests witness the rendered INSERTs, the save/reopen spelling and
  the refusal. The plan's own copy of the range table is corrected to match
  the module (arrow kind, signed tolerances). The edited helper block was
  executed against ezdxf 1.4.4 before being written into the plan.

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-16 21:52:01 +03:00
co-authored by Claude Opus 5
parent 827719fdd0
commit 2e2cd2773d
4 changed files with 236 additions and 13 deletions
@@ -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
+2
View File
@@ -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",
+29 -2
View File
@@ -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.
+87
View File
@@ -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 ────────────────────────────────────────────────────