feat(server): SECTION 18 styles — dimstyle/textstyle/mleaderstyle tools with DIMSTYLE/STYLE/MLEADERSTYLE aliases

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 21:44:02 +03:00
co-authored by Claude Opus 5
parent 266b6fb838
commit a60f801205
6 changed files with 557 additions and 6 deletions
+249
View File
@@ -703,6 +703,7 @@ _GROUP_TAG_PRIORITY = (
"validation",
"analysis",
"block",
"style",
"layer",
"linetype",
"create",
@@ -728,6 +729,7 @@ _GROUP_TAG_LABELS = {
"analysis": "analysis",
"validation": "validation",
"block": "blocks",
"style": "styles",
"layer": "layers",
"linetype": "layers",
"create": "entity_creation",
@@ -6815,6 +6817,253 @@ async def pid_tag_parse(
return parse_tag(tag, kind, equipment_prefixes)
# ---------------------------------------------------------------------------
# ── SECTION 18: Styles (9 tools) ────────────────────────────────────────────
# ---------------------------------------------------------------------------
#
# Pack: core (drafting essentials). Lean profile: dimstyle_set_current and
# textstyle_set_current only (Task 13) — a lean client needs to *use* a
# style a template already holds far more often than to author one.
@cad_tool(
summary="List dimension styles with their ISO-25/ANSI variables and the current one.",
cost="read",
)
@mcp.tool(
annotations={"title": "Styles: List Dimension Styles", "readOnlyHint": True},
tags={"style", "query"},
)
async def dimstyle_list(ctx: Context = None) -> dict:
"""Every DIMSTYLE table entry with the seventeen preset variables
(DIMTXT, DIMASZ, DIMEXE, DIMEXO, DIMGAP, DIMTAD, DIMTIH, DIMTOH, DIMDEC,
DIMDSEP, DIMLUNIT, DIMZIN, DIMBLK, DIMTXSTY, DIMLWD, DIMLWE, DIMSCALE).
Headless, `values` is what the DXF stores (the schema default where an
entry omits a code). On the live engine ActiveX has no per-style getters,
so `values` is read for the *current* style only and the other rows carry
`values: null`, `values_available: false` — `dimstyle_set_current` a
style to read it. Never refuses on an open drawing.
"""
rows = await _backend(ctx).dimstyle_list()
return {
"ok": True,
"styles": rows,
"count": len(rows),
"current": next((row["name"] for row in rows if row["current"]), None),
}
@cad_tool(
summary="Create a dimension style from the ISO-25 or ANSI preset, with per-variable overrides.",
cost="mutate",
)
@mcp.tool(
annotations={"title": "Styles: Create Dimension Style", "readOnlyHint": False},
tags={"style", "create"},
)
async def dimstyle_create(
name: Annotated[str, "New dimension style name, e.g. ISO-25, ANSI, MECH-A3"],
preset: Annotated[
str | None,
"iso-25 (ISO 129-1, 2.5 mm, comma) | ansi (ASME Y14.2, 3 mm, point); omit for the ISO-25 base",
] = None,
overrides: Annotated[
dict | None,
"DIM* variables applied on top of the preset, e.g. {DIMTXT: 3.5, DIMDSEP: '.', DIMTAD: 0}",
] = None,
set_current: Annotated[bool, "Also make it the current dimension style"] = False,
ctx: Context = None,
) -> dict:
"""Create a dimension style: preset values first, then `overrides`.
Refuses, before anything is written: an unknown preset; an override key
outside the 33-variable whitelist (the 17 preset variables plus DIMTOL,
DIMTP, DIMTM, DIMTOLJ, DIMTFAC, DIMLFAC, DIMRND, DIMATFIT, DIMTMOVE,
DIMCLRD/E/T, DIMSAH, DIMBLK1/2, DIMCEN); a value outside its range
(DIMDEC 0-8, sizes > 0, DIMTAD 0-4, DIMDSEP one character, lineweights an
AutoCAD code); a name that already exists (use `dimstyle_modify`); a
DIMTXSTY that is neither an existing text style nor a bundled preset
(ISOCP, ISOCPEUR, ARIAL, ROMANS — those are created, `textstyle_created`).
With `set_current` the next dimension carries the style on both engines.
"""
from engineering.standards.dimstyles import resolve_dimstyle
values = resolve_dimstyle(preset, overrides)
await ctx.info(f"Dimension style {name!r} from preset {preset or 'iso-25'}")
return await _backend(ctx).dimstyle_create(name, values, set_current)
@cad_tool(
summary="Change DIM* variables on an existing dimension style; reports only what moved.",
cost="mutate",
)
@mcp.tool(
annotations={"title": "Styles: Modify Dimension Style", "readOnlyHint": False},
tags={"style", "modify"},
)
async def dimstyle_modify(
name: Annotated[str, "Existing dimension style name"],
overrides: Annotated[dict, "DIM* variables to change, e.g. {DIMDEC: 3, DIMTXT: 3.5}"],
ctx: Context = None,
) -> dict:
"""Change variables on an existing dimension style.
`changed` names only the variables that actually moved — re-setting a
value to itself is not a change. `dimensions_using_style` lists the
dimensions already drawn with it; headlessly `rerender_required: true`
says they still show the old style until redrawn (AutoCAD re-renders on
the next regen). Refuses a missing style, an empty `overrides`, a key
outside the whitelist, a value outside its range, a DIMTXSTY that does
not exist — all before any write. On the live engine, editing a
non-current style makes it current for the duration of the call and the
previous style is restored (its unsaved overrides are discarded, which is
AutoCAD's own DIMSTYLE rule).
"""
await ctx.info(f"Modifying dimension style {name!r}")
return await _backend(ctx).dimstyle_modify(name, overrides)
@cad_tool(summary="Make a dimension style current so new dimensions use it.", cost="safe")
@mcp.tool(
annotations={"title": "Styles: Set Current Dimension Style", "readOnlyHint": False},
tags={"style", "modify"},
)
async def dimstyle_set_current(
name: Annotated[str, "Dimension style to make current (case-insensitive)"],
ctx: Context = None,
) -> dict:
"""Set `$DIMSTYLE` and load the style's variables as the current DIM*
settings — what a live seat's DIMSTYLE Restore does — so `dimension_*`
and `dimension_auto` draw with it on both engines. Per-dimension
tolerances, fits and text overrides still apply on top. Refuses a style
the drawing does not hold. Returns `previous` and `changed`.
"""
return await _backend(ctx).dimstyle_set_current(name)
@cad_tool(summary="List text styles: font file, height, width factor, oblique angle.", cost="read")
@mcp.tool(
annotations={"title": "Styles: List Text Styles", "readOnlyHint": True},
tags={"style", "query"},
)
async def textstyle_list(ctx: Context = None) -> dict:
"""Every STYLE table entry and the one new TEXT/MTEXT will use."""
rows = await _backend(ctx).textstyle_list()
return {
"ok": True,
"styles": rows,
"count": len(rows),
"current": next((row["name"] for row in rows if row["current"]), None),
}
@cad_tool(
summary="Create a text style from a bundled font (isocp, isocpeur, arial, romans) or any file.",
cost="mutate",
)
@mcp.tool(
annotations={"title": "Styles: Create Text Style", "readOnlyHint": False},
tags={"style", "create"},
)
async def textstyle_create(
name: Annotated[str, "New text style name, e.g. ISOCP"],
font: Annotated[
str,
"isocp.shx | isocpeur.ttf | arial.ttf | romans.shx (name or file, any case) or any font file",
],
height: Annotated[
float, Field(default=0.0, ge=0, description="Fixed height; 0 = ask per text")
] = 0.0,
width_factor: Annotated[float, Field(default=1.0, gt=0, description="Width factor")] = 1.0,
oblique_deg: Annotated[float, "Obliquing angle in degrees, -85..85"] = 0.0,
set_current: Annotated[bool, "Also make it the current text style"] = False,
ctx: Context = None,
) -> dict:
"""Create a text style.
A font that is neither a bundled preset nor on the engine's font path is
*written* and reported `font_resolved: false` (AutoCAD substitutes at
open; the headless renderer falls back) — never refused, because a DXF
stores only the name. Refuses, before any write: a name that exists, an
empty font, `width_factor <= 0`, `height < 0`, an oblique angle outside
±85°.
"""
await ctx.info(f"Text style {name!r} with font {font!r}")
return await _backend(ctx).textstyle_create(
name, font, height, width_factor, oblique_deg, set_current
)
@cad_tool(summary="Make a text style current so new text uses it.", cost="safe")
@mcp.tool(
annotations={"title": "Styles: Set Current Text Style", "readOnlyHint": False},
tags={"style", "modify"},
)
async def textstyle_set_current(
name: Annotated[str, "Text style to make current (case-insensitive)"],
ctx: Context = None,
) -> dict:
"""Set `$TEXTSTYLE`; `entity_create_text` / `entity_create_mtext` then
carry the style on both engines. Refuses a style the drawing does not
hold. Returns `previous` and `changed`."""
return await _backend(ctx).textstyle_set_current(name)
@cad_tool(
summary="List multileader styles: arrow size, landing gap, text style and height.",
cost="read",
)
@mcp.tool(
annotations={"title": "Styles: List Multileader Styles", "readOnlyHint": True},
tags={"style", "query"},
)
async def mleaderstyle_list(ctx: Context = None) -> dict:
"""Every MLEADERSTYLE with its four values on both engines
(`values_available: true`). Headless they are read from the MLEADERSTYLE
object; live from the `AcadMLeaderStyle` objects the `ACAD_MLEADERSTYLE`
dictionary holds (ActiveX exposes no *collection* property for them, but
the dictionary items are full objects). `leader_create_mleader` can still
override any of them per leader."""
rows = await _backend(ctx).mleaderstyle_list()
return {"ok": True, "styles": rows, "count": len(rows)}
@cad_tool(
summary="Create a multileader style from the ISO or ANSI preset, on both engines.",
cost="mutate",
)
@mcp.tool(
annotations={"title": "Styles: Create Multileader Style", "readOnlyHint": False},
tags={"style", "create"},
)
async def mleaderstyle_create(
name: Annotated[str, "New multileader style name"],
preset: Annotated[
str, "iso (2.5 mm arrow, 1.0 landing, ISOCP 2.5) | ansi (3.0, 1.5, ROMANS 3.0)"
],
overrides: Annotated[
dict | None, "arrow_size | landing_gap | text_style | text_height on top of the preset"
] = None,
ctx: Context = None,
) -> dict:
"""Create a multileader style: preset values first, then `overrides`.
Headless it lands on `doc.mleader_styles`; live it is added through the
`ACAD_MLEADERSTYLE` dictionary (`AddObject(name, "AcDbMLeaderStyle")`)
and the object's properties are written. Refuses, before any write: an
unknown preset; an override key outside the four; `arrow_size` /
`text_height <= 0`, `landing_gap < 0`; a name that exists; a `text_style`
that is neither present nor a bundled preset (ISOCP, ISOCPEUR, ARIAL,
ROMANS — those are created, `textstyle_created`).
"""
from engineering.standards.mleaderstyles import resolve_mleaderstyle
values = resolve_mleaderstyle(preset, overrides)
await ctx.info(f"Multileader style {name!r} from preset {preset}")
return await _backend(ctx).mleaderstyle_create(name, values)
# ---------------------------------------------------------------------------
# ── RESOURCES ───────────────────────────────────────────────────────────────
# ---------------------------------------------------------------------------