feat(styles): drawing_apply_standard — ISO or ANSI styles, units and layers in one idempotent call

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-21 22:49:12 +03:00
co-authored by Claude Opus 5
parent 021a4eb43d
commit a5d166f365
7 changed files with 269 additions and 10 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co
</div>
> **v1.5 release snapshot:** 175 tools · 8 resources · 5 prompt templates · 2423 collected tests.
> **v1.5 release snapshot:** 176 tools · 8 resources · 5 prompt templates · 2423 collected tests.
> 166 is the **registered** count; a default install advertises 161 over `tools/list`,
> because `ENABLE_3D` is unset. `system_about` is the runtime authority.
+11
View File
@@ -1476,6 +1476,17 @@ TOOL_ALIASES: dict[str, ToolAliases] = {
"activate a dimstyle",
),
),
"drawing_apply_standard": ToolAliases(
acad=(),
synonyms=(
"set the drawing up to iso",
"iso drafting standard",
"ansi drawing setup",
"apply the drafting standard",
"iso-25 and isocp in one go",
"standard styles units and layers",
),
),
"mleaderstyle_create": ToolAliases(
acad=("MLEADERSTYLE",),
synonyms=(
+16 -2
View File
@@ -3,7 +3,7 @@
"schema_version": 1,
"package_version": "1.5.1",
"totals": {
"tools": 175,
"tools": 176,
"resources": 8,
"prompts": 5,
"groups": 21
@@ -24,7 +24,7 @@
"pid": 9,
"premium": 12,
"solids": 5,
"styles": 9,
"styles": 10,
"system": 7,
"templates": 2,
"transactions": 3,
@@ -521,6 +521,20 @@
"destructiveHint": false
}
},
{
"name": "drawing_apply_standard",
"group": "styles",
"cost": "mutate",
"summary": "Put the drawing on ISO or ANSI in one call: dimstyle, text style, units, mech layers.",
"tags": [
"drawing",
"style"
],
"annotations": {
"title": "Drawing: Apply Drafting Standard",
"readOnlyHint": false
}
},
{
"name": "drawing_audit",
"group": "drawing",
+106
View File
@@ -0,0 +1,106 @@
"""One call to put a drawing on a drafting standard: styles, units, layers.
Composes only backend methods that already exist (``textstyle_*``,
``dimstyle_*``, ``system_get/set_variable``, ``drawing_apply_iso_layers``),
so it runs on both engines and needs nothing from the settings facade. Units
are the five system variables both standards share; the decimal separator is
the preset's and arrives through the dimension style. ANSI layer naming is
company-specific, so both standards use the ``mech`` layer set — only styles
and the separator differ (spec §4.2).
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from engineering.standards.dimstyles import resolve_dimstyle
from engineering.standards.textstyles import TEXT_PRESETS
if TYPE_CHECKING:
from backends.base import AutoCADBackend
__all__ = ["STANDARDS", "STANDARD_SYSVARS", "apply_standard", "resolve_standard"]
STANDARDS: dict[str, dict[str, str]] = {
"iso": {"preset": "iso-25", "dimstyle": "ISO-25", "textstyle": "ISOCP", "layer_set": "mech"},
"ansi": {"preset": "ansi", "dimstyle": "ANSI", "textstyle": "ROMANS", "layer_set": "mech"},
}
#: Millimetres, decimal linear and angular units, 1:1 linetype and dimension
#: scale. Written by name so the report can say what moved.
STANDARD_SYSVARS: tuple[tuple[str, Any], ...] = (
("INSUNITS", 4),
("LUNITS", 2),
("AUNITS", 0),
("LTSCALE", 1.0),
("DIMSCALE", 1.0),
)
def resolve_standard(standard: str) -> tuple[str, dict[str, str]]:
"""``(key, row)`` for ``iso`` / ``ansi`` (any case), else ``ValueError``."""
key = str(standard).strip().lower() if standard is not None else ""
if key not in STANDARDS:
raise ValueError(f"standard: unknown value {standard!r}; use one of {sorted(STANDARDS)}")
return key, STANDARDS[key]
def _same(old: Any, new: Any) -> bool:
if old is None:
return False
try:
return abs(float(old) - float(new)) < 1e-9
except (TypeError, ValueError):
return str(old) == str(new)
async def apply_standard(
backend: AutoCADBackend, standard: str, layers: bool = True, units: bool = True
) -> dict:
"""Styles created (or reused) and made current, units written, layers bootstrapped.
Every item reports whether it was created or already present; ``settings``
reports only the variables that moved. An existing style of the standard's
name is used as it is — ``dimstyle_modify`` changes one on purpose.
"""
key, row = resolve_standard(standard)
if not isinstance(layers, bool) or not isinstance(units, bool):
raise TypeError("layers and units must be booleans")
text_names = {entry["name"].lower() for entry in await backend.textstyle_list()}
textstyle_created = row["textstyle"].lower() not in text_names
if textstyle_created:
font_file, width, oblique = TEXT_PRESETS[row["textstyle"]]
await backend.textstyle_create(row["textstyle"], font_file, 0.0, width, oblique, False)
await backend.textstyle_set_current(row["textstyle"])
dim_names = {entry["name"].lower() for entry in await backend.dimstyle_list()}
dimstyle_created = row["dimstyle"].lower() not in dim_names
if dimstyle_created:
await backend.dimstyle_create(
row["dimstyle"], resolve_dimstyle(row["preset"], None), set_current=True
)
else:
await backend.dimstyle_set_current(row["dimstyle"])
changed: dict[str, list] = {}
if units:
for var, value in STANDARD_SYSVARS:
old = await backend.system_get_variable(var)
if _same(old, value):
continue
await backend.system_set_variable(var, value)
changed[var] = [old, value]
layer_result = None
if layers:
applied = await backend.drawing_apply_iso_layers(row["layer_set"])
layer_result = {"layer_set": row["layer_set"], "layers": applied["layers"]}
return {
"standard": key,
"dimstyle": {"name": row["dimstyle"], "created": dimstyle_created, "current": True},
"textstyle": {"name": row["textstyle"], "created": textstyle_created, "current": True},
"settings": {"changed": changed, "applied": units},
"layers": layer_result,
}
+37 -1
View File
@@ -6818,7 +6818,7 @@ async def pid_tag_parse(
# ---------------------------------------------------------------------------
# ── SECTION 18: Styles (9 tools) ────────────────────────────────────────────
# ── SECTION 18: Styles (10 tools) ───────────────────────────────────────────
# ---------------------------------------------------------------------------
#
# Pack: core (drafting essentials). Lean profile: dimstyle_set_current and
@@ -7076,6 +7076,42 @@ async def mleaderstyle_create(
return await _backend(ctx).mleaderstyle_create(name, values)
@cad_tool(
summary="Put the drawing on ISO or ANSI in one call: dimstyle, text style, units, mech layers.",
cost="mutate",
)
@mcp.tool(
annotations={"title": "Drawing: Apply Drafting Standard", "readOnlyHint": False},
tags={"style", "drawing"},
)
async def drawing_apply_standard(
standard: Annotated[
str, "iso (ISO-25 / ISOCP, decimal comma) | ansi (ANSI / ROMANS, decimal point)"
],
layers: Annotated[bool, "Also bootstrap the mech layer set with ISO 128 lineweights"] = True,
units: Annotated[
bool, "Also write INSUNITS mm, LUNITS/AUNITS decimal, LTSCALE 1, DIMSCALE 1"
] = True,
ctx: Context = None,
) -> dict:
"""One call for the common case: the standard's dimension style (created
if missing, then current), its text style (created if missing, then
current), the shared units, and with `layers` the `mech` layer set (ANSI
layer naming is company-specific, so both standards share it).
Every item reports `created` or already present; `settings.changed`
names only the variables that moved, so a second call reports nothing.
An existing ISO-25 / ANSI style is reused as it is, not reset — use
`dimstyle_modify` to change one. Refuses an unknown standard before any
write; the underlying style refusals (`dimstyle_create`,
`textstyle_create`) apply unchanged. In the `lean` profile (Task 13).
"""
from engineering.standards.apply import apply_standard
await ctx.info(f"Applying the {standard} drafting standard")
return await apply_standard(_backend(ctx), standard, layers, units)
# ---------------------------------------------------------------------------
# ── RESOURCES ───────────────────────────────────────────────────────────────
# ---------------------------------------------------------------------------
+91 -1
View File
@@ -999,7 +999,9 @@ def com_backend(monkeypatch, tmp_path):
monkeypatch.setattr(_FakeStyle, "support_folder", support)
document = _FakeDocument()
app = types.SimpleNamespace(
Preferences=types.SimpleNamespace(Files=types.SimpleNamespace(SupportPath=str(support)))
Preferences=types.SimpleNamespace(Files=types.SimpleNamespace(SupportPath=str(support))),
GetVariable=document.GetVariable,
SetVariable=document.SetVariable,
)
monkeypatch.setattr(module, "_acad_doc", lambda: document)
monkeypatch.setattr(module, "_acad_app", lambda: app)
@@ -1626,3 +1628,91 @@ async def test_truetype_face_reads_family_and_subfamily(tmp_path):
assert _truetype_face(windows_arial) == ("Arial", False, False)
assert _com_font_path(str(regular)) == regular
assert _com_font_path(str(tmp_path / "missing.ttf")) is None
# ── drawing_apply_standard ──────────────────────────────────────────────────
async def test_apply_iso_then_dimension_linear_carries_iso25(backend):
from engineering.standards.apply import apply_standard
result = await apply_standard(backend, "iso")
assert result["standard"] == "iso"
assert result["dimstyle"] == {"name": "ISO-25", "created": True, "current": True}
assert result["textstyle"] == {"name": "ISOCP", "created": True, "current": True}
assert result["settings"]["changed"] == {"INSUNITS": [6, 4]}, "ezdxf.new() starts in metres"
assert result["layers"]["layer_set"] == "mech"
assert result["layers"]["layers"]["DIM"] in ("created", "exists")
dim = await backend.dimension_linear(0, 0, 33.333, 0, 16, -30)
assert _rendered(backend, dim.handle) == ("33,33", 2.5, [2.5])
assert backend._doc.header["$TEXTSTYLE"] == "ISOCP"
assert await backend.system_get_variable("LUNITS") == 2
assert await backend.system_get_variable("INSUNITS") == 4
async def test_apply_ansi_then_dimension_linear_carries_ansi(backend):
from engineering.standards.apply import apply_standard
result = await apply_standard(backend, "ANSI", layers=False)
assert result["dimstyle"]["name"] == "ANSI" and result["textstyle"]["name"] == "ROMANS"
assert result["layers"] is None
dim = await backend.dimension_linear(0, 0, 33.333, 0, 16, -30)
assert _rendered(backend, dim.handle) == ("33.33", 3.0, [3.0])
async def test_second_apply_is_idempotent_and_reports_nothing_created(backend):
from engineering.standards.apply import apply_standard
await apply_standard(backend, "iso")
again = await apply_standard(backend, "iso")
assert again["dimstyle"]["created"] is False and again["textstyle"]["created"] is False
assert again["settings"]["changed"] == {}
assert set(again["layers"]["layers"].values()) == {"exists"}
swapped = await apply_standard(backend, "ansi", layers=False)
assert swapped["dimstyle"]["created"] is True
assert (await backend.dimstyle_list())[0]["name"] == "ANSI"
back = await apply_standard(backend, "iso", layers=False, units=False)
assert back["dimstyle"]["created"] is False and back["settings"] == {
"changed": {},
"applied": False,
}
assert backend._doc.header["$DIMSTYLE"] == "ISO-25"
async def test_apply_refuses_an_unknown_standard_before_writing(backend):
from engineering.standards.apply import apply_standard
with pytest.raises(ValueError, match="standard"):
await apply_standard(backend, "din")
with pytest.raises(TypeError, match="layers"):
await apply_standard(backend, "iso", layers="yes")
assert [r["name"] for r in await backend.dimstyle_list()] == ["Standard"]
assert await backend.system_get_variable("INSUNITS") == 6
async def test_apply_standard_tool_is_registered_in_the_styles_group(backend):
import server
names = {t.name for t in await server._registered_tools() if getattr(t, "name", None)}
assert "drawing_apply_standard" in names
styles_group = set((await server._tool_groups())["styles"])
assert styles_group == STYLE_TOOLS | {"drawing_apply_standard"}, "SECTION 18 is exactly ten"
result = await server.drawing_apply_standard(standard="iso", layers=False, ctx=_Ctx(backend))
assert result["dimstyle"]["created"] is True
async def test_com_apply_standard_creates_both_styles_and_sets_units(com_backend):
from engineering.standards.apply import apply_standard
backend, document = com_backend
document.variables.update(
{"INSUNITS": 1, "LUNITS": 4, "AUNITS": 0, "LTSCALE": 1.0, "DIMSCALE": 1.0}
)
result = await apply_standard(backend, "iso", layers=False)
assert result["dimstyle"]["created"] is True and result["textstyle"]["created"] is True
assert result["settings"]["changed"] == {"INSUNITS": [1, 4], "LUNITS": [4, 2]}
names = [call[0] for call in document.calls]
assert names[0] == "TextStyles.Add" and "DimStyles.Add" in names
assert ("ActiveTextStyle", "ISOCP") in document.calls
assert document.ActiveDimStyle.Name == "ISO-25"
assert document.variables["DIMDSEP"] == "," and document.variables["INSUNITS"] == 4
+7 -5
View File
@@ -519,9 +519,11 @@ async def test_tool_group_sizes_are_unchanged():
tag is ranked first in `_GROUP_TAG_PRIORITY` so those tools file under
`pid` rather than under their secondary `query` / `create` tags. `styles`
appeared (0 -> 9) when track E's SECTION 18 opened with the dimension,
text and multileader style tools; the `style` tag sits ahead of `layer`
in `_GROUP_TAG_PRIORITY` so they never file under their secondary
`query` / `create` / `modify` tags. Every other number here has been
text and multileader style tools, then 9 -> 10 when
`drawing_apply_standard` (ISO or ANSI styles, units and layers in one
call) joined; the `style` tag sits ahead of `layer` and `drawing` in
`_GROUP_TAG_PRIORITY` so they never file under their secondary
`query` / `create` / `modify` / `drawing` tags. Every other number here has been
unchanged since the snapshot was taken.
"""
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
@@ -541,11 +543,11 @@ async def test_tool_group_sizes_are_unchanged():
"pid": 9,
"premium": 12,
"solids": 5,
"styles": 9,
"styles": 10,
"system": 7,
"templates": 2,
"transactions": 3,
"validation": 1,
"view": 4,
}
assert sum(sizes.values()) == 175
assert sum(sizes.values()) == 176