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:
co-authored by
Claude Opus 5
parent
021a4eb43d
commit
a5d166f365
@@ -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.
|
||||
|
||||
|
||||
@@ -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=(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user