merge: track-e group P

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-22 11:14:02 +03:00
co-authored by Claude Opus 5
28 changed files with 24378 additions and 46 deletions
+277 -10
View File
@@ -1090,7 +1090,7 @@ async def drawing_info(ctx: Context) -> dict:
@cad_tool(
summary="Start a blank drawing, pre-seeded with the standard engineering layers.",
summary="Start a drawing: blank with the engineering layers, or from a bundled/own template.",
cost="mutate",
)
@mcp.tool(
@@ -1098,7 +1098,11 @@ async def drawing_info(ctx: Context) -> dict:
tags={"drawing"},
)
async def drawing_new(
template: Annotated[str | None, "Optional path to .dwt template file"] = None,
template: Annotated[
str | None,
"Bundled template name (drawing_template_list: iso_a3_mech, iso_a1_arch, iso_a3_pid, "
"ansi_b_mech, ansi_d_arch) or a path to a .dwt/.dxf template file.",
] = None,
bootstrap: Annotated[
bool,
Field(
@@ -1109,19 +1113,54 @@ async def drawing_new(
] = True,
ctx: Context = None,
) -> dict:
"""Create a new empty drawing, optionally from a template (.dwt).
"""Create a new drawing, optionally from a template.
When ``bootstrap=True`` (default), the drawing is also seeded with the
standard engineering linetypes (CENTER/HIDDEN/PHANTOM) and layers
(GEOMETRY, DIM, CENTER, HIDDEN, PHANTOM, HATCH, TEXT, TITLEBLOCK).
A bundled name resolves to `templates/<name>.dxf` headlessly and
`templates/<name>.dwt` on the live engine (falling back to the DXF with
`source: "bundled_dxf"` when no `.dwt` twin is committed); a path is used
as given. The result carries `template: {name|path, path, source}`.
On the live engine only a genuine `.dwt` reaches `Documents.Add` —
measured on AutoCAD 2026, `Add(<file.dxf>)` silently returns the default
drawing — so a `.dxf` template (bundled or a path) is first converted
through AutoCAD into a cached `.dwt` and `template.dwt` names it
(`template.dwt_cached` says whether the conversion was reused). The
drawing then really carries the template's layers, tabs and page setup.
Refused before anything is replaced: a bare name that is not in the
catalogue (the message lists the five), a template path that does not
exist, and a path outside the allowed directories. With `bootstrap=True`
(default) the standard engineering linetypes and layers are ensured
afterwards — idempotent on a template that already has them.
"""
if template is not None:
validated_template = validate_path(template, allow_write=False)
template = str(validated_template)
await ctx.info(f"Creating new drawing (template={template}, bootstrap={bootstrap})")
from pathlib import Path as _P
from engineering.standards.templates import resolve_template
backend = _backend(ctx)
template_info: dict | None = None
if template is not None:
try:
resolved, source = resolve_template(template, backend.name)
except ValueError as exc:
raise ToolError(f"drawing_new: {exc}") from exc
if source == "path":
resolved = str(validate_path(resolved, allow_write=False))
if not _P(resolved).is_file():
raise ToolError(f"drawing_new: template file not found: {resolved}")
template_info = {"path": resolved, "source": source}
else:
template_info = {"name": template.strip().lower(), "path": resolved, "source": source}
template = resolved
await ctx.info(f"Creating new drawing (template={template}, bootstrap={bootstrap})")
raw = await backend.drawing_new(template)
result = dict(raw) if isinstance(raw, dict) else {"result": raw}
converted = result.pop("template_dwt", None)
if template_info is not None:
if converted: # the live engine built a .dwt from the .dxf (see ComBackend.drawing_new)
template_info["dwt"] = converted["path"]
template_info["dwt_cached"] = bool(converted.get("cached"))
result["template"] = template_info
if bootstrap:
try:
from engineering import (
@@ -7230,6 +7269,234 @@ async def drawing_apply_standard(
return await apply_standard(_backend(ctx), standard, layers, units)
# ---------------------------------------------------------------------------
# ── SECTION 19: Page Setup & Templates (6 tools) ────────────────────────────
# ---------------------------------------------------------------------------
@cad_tool(
summary="Read each sheet's page setup: paper, orientation, ctb, scale, device.",
cost="read",
)
@mcp.tool(
annotations={"title": "List Page Setups", "readOnlyHint": True},
tags={"layout", "plot", "query"},
)
async def page_setup_list(
layout: Annotated[
str | None, "One paper-space layout, or omit for every sheet (Model is never listed)."
] = None,
ctx: Context = None,
) -> dict:
"""The page setup stored on each paper-space layout.
`paper` is the catalogue name (ISO_A3, ANSI_B, …) when the stored size
matches one within 0.5 mm, otherwise the media name as stored; `size_mm`,
`orientation`, `plot_style` (ctb), `scale` ("fit", "1:50", or a custom
ratio), `plot_area`, `device`, `margins_mm` [top, bottom, left, right] and
`center` are read back from the LAYOUT object (headless) or the AutoCAD
Layout (live). Refuses an unknown layout name and `Model` (model space has
no sheet; plot it with `drawing_export_pdf(layout=None)`).
"""
try:
rows = await _backend(ctx).page_setup_list(layout)
except ValueError as exc:
raise ToolError(str(exc)) from exc
return {"ok": True, "layouts": rows, "count": len(rows)}
@cad_tool(
summary="Set a sheet's paper, orientation, ctb, plot scale and device (PAGESETUP).",
cost="safe",
)
@mcp.tool(
annotations={"title": "Apply Page Setup", "destructiveHint": False},
tags={"layout", "plot"},
)
async def page_setup_apply(
layout: Annotated[str, "Paper-space layout to set up (never 'Model')."],
paper: Annotated[str, "ISO_A0…ISO_A4 or ANSI_A…ANSI_E (short forms A3, ansi_b accepted)."],
orientation: Annotated[str, "landscape | portrait"] = "landscape",
plot_style: Annotated[
str, "Plot style table, e.g. monochrome.ctb, acad.ctb, Grayscale.ctb (plot_style_list)."
] = "monochrome.ctb",
scale: Annotated[
str, "fit | 1:1 | 1:2 | 1:5 | 1:10 | 1:20 | 1:50 | 1:100 | 2:1 | 5:1 | 10:1"
] = "fit",
plot_area: Annotated[str, "layout | extents"] = "layout",
device: Annotated[str, "Plotter configuration (.pc3) or printer name."] = "DWG To PDF.pc3",
margins_mm: Annotated[
list[float] | None,
"[top, bottom, left, right] in mm. Headless only: AutoCAD takes margins from the .pc3.",
] = None,
center: Annotated[bool, "Centre the plot on the paper."] = True,
ctx: Context = None,
) -> dict:
"""AutoCAD's PAGESETUP as one call, on both engines.
Paper sizes are ISO 216 / ANSI Y14.1; the media is written in AutoCAD's own
spelling (`ISO_A3_(420.00_x_297.00_MM)`). `changed` lists only the values
that moved — re-applying the same setup reports `{}`. An unknown ctb is
written and reported `plot_style_known: false` (a missing ctb only matters
at plot time). Viewports on the sheet are never touched.
Refused before anything is written: an unknown paper, orientation, scale
or plot_area, an empty device or plot_style, margins that leave no
printable area, `Model`, an unknown layout. On the live engine `margins_mm`
is refused (the .pc3 owns them) and a media the device does not offer is
refused with the device's names for the same paper.
The proof is the PDF: `batch_plot` reads each sheet's `/MediaBox` back.
"""
from engineering.standards.papers import resolve_page_setup
try:
setup = resolve_page_setup(
paper, orientation, plot_style, scale, plot_area, device, margins_mm, center
)
await ctx.info(f"Applying page setup {setup['paper']} {orientation} to {layout}")
return await _backend(ctx).page_setup_apply(layout, setup)
except ValueError as exc:
raise ToolError(str(exc)) from exc
@cad_tool(
summary="List the plot style tables (ctb): the AutoCAD catalogue, plus what is installed.",
cost="read",
)
@mcp.tool(
annotations={"title": "List Plot Styles", "readOnlyHint": True},
tags={"layout", "plot", "query"},
)
async def plot_style_list(ctx: Context = None) -> dict:
"""The ctb files AutoCAD ships (`monochrome.ctb`, `acad.ctb`, `Grayscale.ctb`,
the Screening set, …) and, on the live engine, the files actually present
in `Preferences.Files.PrinterStyleSheetPath` (`source: "installed"`,
`installed: true/false` per row). Headlessly `installed` is `null`: there
is no installation to scan, and the catalogue is not evidence of one.
Never refuses; an unreadable Preferences object degrades to the catalogue.
"""
backend = _backend(ctx)
rows = await backend.plot_style_list()
return {"ok": True, "styles": rows, "count": len(rows), "backend": backend.name}
@cad_tool(
summary="Plot every sheet (or the named ones) to PDF; each sheet size is read back from its PDF.",
cost="mutate",
)
@mcp.tool(
annotations={"title": "Batch Plot", "destructiveHint": False},
tags={"layout", "plot", "export"},
)
async def batch_plot(
output_dir: Annotated[str, "Folder for the PDFs (created if missing)."],
layouts: Annotated[
list[str] | None,
"Layouts to plot; omit for every paper-space layout. 'Model' only when named.",
] = None,
format: Annotated[str, "pdf (the only format this release plots)."] = "pdf",
file_pattern: Annotated[
str, "File name per sheet; {drawing} and {layout} are substituted."
] = "{drawing}-{layout}.pdf",
ctx: Context = None,
) -> dict:
"""AutoCAD's PUBLISH for a PDF set, through `drawing_export_pdf(layout=…)`.
One row per sheet: `path`, `bytes`, and `mediabox_mm` **parsed from the
PDF that was written** (`/MediaBox`, points → mm), with `paper` naming the
catalogue size it matches within 0.5 mm — so the sheet size is verified,
never assumed. Model space is included only when named.
Refused before any file is written: a format other than pdf, an empty
`layouts` list, a layout that does not exist, a `file_pattern` containing a
path separator or lacking `{layout}` when more than one sheet is plotted,
and any output path outside the allowed directories. A sheet whose PDF has
no readable MediaBox is reported with `mediabox_mm: null` and an `error`.
"""
from engineering.standards.plot import batch_plot as _batch_plot
if str(format).lower() != "pdf":
raise ToolError(
f"batch_plot: format must be 'pdf' (got {format!r}); PDF is the only plot format"
)
destination = validate_path(output_dir, allow_write=True)
await ctx.info(f"Batch plotting {layouts or 'every sheet'} to {destination}")
try:
return await _batch_plot(_backend(ctx), layouts, str(destination), file_pattern)
except ValueError as exc:
raise ToolError(str(exc)) from exc
@cad_tool(
summary="List the five bundled drawing templates: standard, sheet, layers, styles, page setup.",
cost="read",
)
@mcp.tool(
annotations={"title": "List Drawing Templates", "readOnlyHint": True},
tags={"template", "drawing", "query"},
)
async def drawing_template_list(ctx: Context = None) -> dict:
"""The templates `drawing_new(template=<name>)` can start from.
Each row: `standard` (ISO/ANSI), `sheet`, `paper`, `layout` (the sheet tab's
name), `layer_set` (mech / pid / iso13567), `dimstyle` / `textstyle`
(ISO-25 / ISOCP or ANSI / ROMANS), `scale`, `title_block` (the ISO 5457 A3
frame, A3 sheets only), the `settings` applied, and `files.dxf` /
`files.dwt` with `present` flags — the `.dwt` twins exist only once built
on a live AutoCAD (`scripts/smoke_settings_com.py --build-dwt`). Never
refuses; it reads a catalogue.
"""
from engineering.standards.templates import TEMPLATES_DIR, template_rows
rows = template_rows()
return {
"ok": True,
"templates": rows,
"count": len(rows),
"templates_dir": str(TEMPLATES_DIR),
"engine": _backend(ctx).name,
}
@cad_tool(
summary="Save the current drawing as a template: .dwt on live AutoCAD, .dxf headlessly.",
cost="mutate",
)
@mcp.tool(
annotations={"title": "Save As Template", "destructiveHint": False},
tags={"template", "drawing"},
)
async def drawing_template_save(
path: Annotated[str, "Destination .dwt (live AutoCAD) or .dxf (either engine)."],
name: Annotated[str | None, "Template name for the report (default: the file stem)."] = None,
description: Annotated[
str | None, "Template description → DWT summary Comments on the live engine."
] = None,
ctx: Context = None,
) -> dict:
"""AutoCAD's SAVEAS → Drawing Template, on both engines.
Live AutoCAD writes a real `.dwt` (`SaveAs(path, ac2018_Template)`) and
puts `description` into the template's summary Comments; the active
document is rebound to the new file, as SAVEAS does. Headlessly a `.dxf`
template is written — `drawing_new(template=<path>)` opens it — and a
`.dwt` request is refused with capability `dwt_write` (the message names
the DXF route and the COM route); a description is reported
`description_written: false` because DXF has nowhere to keep it. Any other
suffix, and any path outside the allowed directories, is refused before
a byte is written.
Pack: core · lean: no (`drawing_save_as` is not lean either).
"""
validated = validate_path(path, allow_write=True)
await ctx.info(f"Saving template: {validated}")
try:
return await _backend(ctx).drawing_template_save(str(validated), name, description)
except ValueError as exc:
raise ToolError(str(exc)) from exc
# ---------------------------------------------------------------------------
# ── SECTION 20: Environment (3 tools) ───────────────────────────────────────
# ---------------------------------------------------------------------------