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>
|
</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`,
|
> 166 is the **registered** count; a default install advertises 161 over `tools/list`,
|
||||||
> because `ENABLE_3D` is unset. `system_about` is the runtime authority.
|
> because `ENABLE_3D` is unset. `system_about` is the runtime authority.
|
||||||
|
|
||||||
|
|||||||
@@ -1476,6 +1476,17 @@ TOOL_ALIASES: dict[str, ToolAliases] = {
|
|||||||
"activate a dimstyle",
|
"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(
|
"mleaderstyle_create": ToolAliases(
|
||||||
acad=("MLEADERSTYLE",),
|
acad=("MLEADERSTYLE",),
|
||||||
synonyms=(
|
synonyms=(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"package_version": "1.5.1",
|
"package_version": "1.5.1",
|
||||||
"totals": {
|
"totals": {
|
||||||
"tools": 175,
|
"tools": 176,
|
||||||
"resources": 8,
|
"resources": 8,
|
||||||
"prompts": 5,
|
"prompts": 5,
|
||||||
"groups": 21
|
"groups": 21
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"pid": 9,
|
"pid": 9,
|
||||||
"premium": 12,
|
"premium": 12,
|
||||||
"solids": 5,
|
"solids": 5,
|
||||||
"styles": 9,
|
"styles": 10,
|
||||||
"system": 7,
|
"system": 7,
|
||||||
"templates": 2,
|
"templates": 2,
|
||||||
"transactions": 3,
|
"transactions": 3,
|
||||||
@@ -521,6 +521,20 @@
|
|||||||
"destructiveHint": false
|
"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",
|
"name": "drawing_audit",
|
||||||
"group": "drawing",
|
"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
|
# 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)
|
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 ───────────────────────────────────────────────────────────────
|
# ── RESOURCES ───────────────────────────────────────────────────────────────
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
+91
-1
@@ -999,7 +999,9 @@ def com_backend(monkeypatch, tmp_path):
|
|||||||
monkeypatch.setattr(_FakeStyle, "support_folder", support)
|
monkeypatch.setattr(_FakeStyle, "support_folder", support)
|
||||||
document = _FakeDocument()
|
document = _FakeDocument()
|
||||||
app = types.SimpleNamespace(
|
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_doc", lambda: document)
|
||||||
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
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 _truetype_face(windows_arial) == ("Arial", False, False)
|
||||||
assert _com_font_path(str(regular)) == regular
|
assert _com_font_path(str(regular)) == regular
|
||||||
assert _com_font_path(str(tmp_path / "missing.ttf")) is None
|
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
|
tag is ranked first in `_GROUP_TAG_PRIORITY` so those tools file under
|
||||||
`pid` rather than under their secondary `query` / `create` tags. `styles`
|
`pid` rather than under their secondary `query` / `create` tags. `styles`
|
||||||
appeared (0 -> 9) when track E's SECTION 18 opened with the dimension,
|
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`
|
text and multileader style tools, then 9 -> 10 when
|
||||||
in `_GROUP_TAG_PRIORITY` so they never file under their secondary
|
`drawing_apply_standard` (ISO or ANSI styles, units and layers in one
|
||||||
`query` / `create` / `modify` tags. Every other number here has been
|
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.
|
unchanged since the snapshot was taken.
|
||||||
"""
|
"""
|
||||||
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
|
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,
|
"pid": 9,
|
||||||
"premium": 12,
|
"premium": 12,
|
||||||
"solids": 5,
|
"solids": 5,
|
||||||
"styles": 9,
|
"styles": 10,
|
||||||
"system": 7,
|
"system": 7,
|
||||||
"templates": 2,
|
"templates": 2,
|
||||||
"transactions": 3,
|
"transactions": 3,
|
||||||
"validation": 1,
|
"validation": 1,
|
||||||
"view": 4,
|
"view": 4,
|
||||||
}
|
}
|
||||||
assert sum(sizes.values()) == 175
|
assert sum(sizes.values()) == 176
|
||||||
|
|||||||
Reference in New Issue
Block a user