Add AutoCAD MCP premium skill and backend APIs

Introduce the autocad-mcp-premium skill with workflow docs, examples, and references (.claude/skills/autocad-mcp-premium/*). Add PlanSpec/critique plumbing and engineering helpers (engineering/plan_spec.py, engineering/critique.py) plus tests and a smoke script. Extend backend API (backends/base.py) with corner operations and premium meta-tools (trim/extend/fillet/chamfer, drawing_plan, drawing_critique, point_from_snap, construction_xline/clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart) and implement COM backend corner ops (backends/com_backend.py) with placeholders for meta-tools. Update CLAUDE.md to document the premium drawing rules and standard workflow. Misc: add examples, references, and tests for premium features, and minor server/backend adjustments.
This commit is contained in:
U-C4N
2026-05-15 19:17:14 +03:00
parent 88a9005a6a
commit 9258da429f
19 changed files with 2847 additions and 2 deletions
@@ -0,0 +1,77 @@
# Corner Operations Reference
V1 supports **LINE+LINE** only. Other entity combinations (line+arc, line+circle,
polyline-segment) raise `RuntimeError` and will land in V2/V3.
## Decision tree
```text
┌────── two lines should meet at a sharp corner
│ → entity_trim (when one line overshoots)
│ → entity_extend (when one line falls short)
│
Need to close ─┼────── two lines should meet at a rounded corner (R>0)
a corner? │ → entity_fillet
│
└────── two lines should meet at an angled bevel
→ entity_chamfer
```
## `entity_trim(target_handle, cutter_handle, keep_x, keep_y)`
- `target` — the line being shortened.
- `cutter` — treated as an **infinite ray** (AutoCAD's "implied extend" trim).
- `keep_x/keep_y` — a point on the side of `target` you want to KEEP.
- Returns the (in-place modified) target's `EntityInfo`.
**Tip:** use `point_from_snap(target_handle, "mid")` or another snap to compute
`keep_x/keep_y` deterministically.
## `entity_extend(target_handle, boundary_handle, end_x=None, end_y=None)`
- Extends `target` until it hits `boundary` (treated as infinite).
- `end_x/end_y=None` → auto-pick the target endpoint nearest the boundary midpoint.
- Pass `end_x/end_y` explicitly to disambiguate when you need to extend a
specific endpoint.
## `entity_fillet(handle1, handle2, radius, trim=True)`
- Inserts a tangent **ARC** between the two lines.
- `radius=0` → corner-merge (no arc, only trim).
- `trim=False` → leave source lines untouched (rare; AutoCAD default is True).
- Returns the new ARC handle (or the first source line for `radius=0`).
## `entity_chamfer(handle1, handle2, dist1, dist2=None, trim=True)`
- Inserts a straight chamfer **LINE** between the two source lines.
- `dist2=None` → symmetric (`dist2 = dist1`); supply for asymmetric chamfers.
- Returns the new chamfer LINE handle.
## Corner-case table
| Vaka | Behaviour |
|------|-----------|
| Lines are parallel / anti-parallel | `RuntimeError("…parallel…")` |
| Same handle for both | `RuntimeError("…same…")` |
| One handle is not a LINE | `RuntimeError("…LINE+LINE only…")` |
| Intersection beyond segment ends | trim/fillet still work (implied extend) |
| Negative radius / zero distance | `RuntimeError` |
| Tiny gap (< 0.5 mm) between lines | flagged by `drawing_critique` as `untrimmed_corner` |
## Worked example — L-bracket inner corner R5
```python
l1 = await entity_create_line(0, 0, 100, 0) # bottom edge
l2 = await entity_create_line(100, 0, 100, 80) # right edge
arc = await entity_fillet(l1.handle, l2.handle, radius=5)
# arc.center == (95, 5); arc.radius == 5
# l1.end is now (95, 0); l2.start is now (100, 5)
```
## Why this matters
LLMs without these tools compute corner geometry by hand: they pick endpoints,
solve intersections, and write coordinates. That fails the moment a line is
slightly off, an angle is non-orthogonal, or the user changes a length. The
corner ops + `point_from_snap` move that math out of the LLM's head and into
deterministic library code.
@@ -0,0 +1,109 @@
# ISO Standards Reference
Distilled tables for the standards this skill enforces.
## ISO 128 — Line widths
Allowed lineweight set (mm), with 4:2:1 ratio rule:
| Width | Common use |
|-------|------------|
| 0.13 | Hatch, dim text shadow |
| 0.18 | Hidden, center, phantom, dim text |
| 0.25 | Default text, leader |
| 0.35 | Auxiliary visible (where contrast needed) |
| 0.50 | Visible solid edges (most common production weight) |
| 0.70 | Heavy solid (large-scale parts), P&ID main process |
| 1.00 | Border, key heavy outlines |
| 1.40 | Title borders |
| 2.00 | Sheet outer border |
**Rule:** every layer's lineweight must be one of these (or 0.0 = "Default" =
inherits from active style). Anything else fails `drawing_critique` focus
`iso128`.
## ISO 128 — Line types
| Type | Linetype name | Used for |
|------|---------------|----------|
| Continuous thick | `Continuous` (0.50+ mm) | Visible edges |
| Dashed thin | `HIDDEN` (0.18–0.25) | Hidden edges |
| Chain thin | `CENTER` (0.18) | Centerlines, axes of symmetry |
| Chain double-dashed | `PHANTOM` (0.18) | Section / cutting plane / alternate position |
| Continuous thin | `Continuous` (0.18–0.25) | Dimensions, hatch, leaders |
## ISO 13567 — Layer naming (4-field)
Format: `<Agent2c>-<Element6c>-<Presentation2c>-<Status1c>`
| Field | Length | Examples |
|-------|--------|----------|
| Agent | 2 chars | A=Architectural, S=Structural, M=Mechanical, P=Plumbing, E=Electrical |
| Element | 6 chars | GEOMET=geometry, HIDDEN=hidden edges, DIMEN=dimensions, TITLE=title block |
| Presentation | 2 chars | E=edge, T=text, H=hatch |
| Status | 1 char | N=new, E=existing, D=demolish |
Example layer set ships in `engineering/layers.py::ISO13567_LAYERS`.
## ISO 129-1 — Dimensioning rules
1. **Each dimension is given once** — never repeat.
2. **Extension lines** project at least 8× line width past the dim line.
3. **Decimal separator** is the comma (per ISO; we accept point too).
4. **Text orientation**: read from below the drawing or from the right side.
5. **Font**: ISO 3098 (`isocp.shx` ships with AutoCAD).
6. **Dimension styles**: chain, baseline, ordinate are the three standard
layouts — choose one per drawing and stick to it (`dim_style` in PlanSpec).
## ISO 7200 — Title-block fields
Mandatory:
- Legal owner (Anka-Makine)
- Drawing number
- Title
- Date
- Sheet / total sheets
- Approver name + signature
- Creator name
- Document type
Common optional:
- Revision
- Scale (matches PlanSpec.scale)
- Projection symbol (1st angle ISO E vs 3rd angle ISO A)
This skill assumes you call `titleblock_apply_iso_a3` (or its A3 cousin) to
populate these fields after `drawing_finalize`'s validator runs.
## ISO 14617 / 10628 — P&ID symbology
When `layer_set_id="pid"` is passed to `drawing_plan`, the bootstrap creates
the P&ID layer set:
| Layer | Linetype | Lineweight | Purpose |
|-------|----------|------------|---------|
| `PROCESS-PIPING-MAIN` | Continuous | 0.70 | Main process line (heavy) |
| `PROCESS-PIPING-SECONDARY` | Continuous | 0.50 | Secondary process line |
| `PROCESS-EQUIPMENT` | Continuous | 0.50 | Vessels / pumps / drums |
| `PROCESS-VALVES` | Continuous | 0.35 | Valves & fittings |
| `INSTRUMENT-SYMBOL` | Continuous | 0.35 | Instrument bubbles (FCFs) |
| `INSTRUMENT-LINE-SIGNAL` | DASHED | 0.18 | Pneumatic / electrical signal |
| `INSTRUMENT-TAG-TEXT` | Continuous | 0.25 | Tag labels |
| `ELECTRICAL-LINE` | DASHDOT | 0.18 | Electrical line |
| `UTILITY-LINE` | PHANTOM | 0.25 | Steam / water / air utility |
| `INSULATION-HATCH` | Continuous | 0.13 | Insulation hatching |
### ISA-5.1 tag conventions (P&ID instruments)
`<First letter><Second letter>-<Loop number><Suffix?>`
| First letter (measured variable) | Second letter (function) |
|----------------------------------|--------------------------|
| F = Flow | T = Transmitter |
| P = Pressure | I = Indicator |
| T = Temperature | C = Controller |
| L = Level | V = Valve |
| A = Analytical | S = Switch |
Examples: `FT-101` (flow transmitter, loop 101), `PCV-203A` (pressure control
valve, loop 203, A spare).
@@ -0,0 +1,113 @@
# Meta-Planning Reference
The premium meta-tool layer wraps the entity primitives in a quality-first
workflow. Schemas live in `engineering/plan_spec.py`; checks live in
`engineering/critique.py`.
## `drawing_plan(intent, sheet_size, scale, layer_set_id, view_count, dim_style, notes)`
Commits a `PlanSpec` to the backend instance. Always call FIRST.
```yaml
PlanSpec:
intent: str # "L-bracket 50x80"
sheet_size: A4 | A3 | A2 | A1 | A0 # default A3
scale: float # 1.0 = 1:1
layer_set_id: mech | pid | iso13567 # default mech
view_count: int # default 1
dim_style: chain | baseline | ordinate | mixed # default chain
notes: list[str] # free-form constraints
```
The PlanSpec is held by the backend (not persisted to DXF) and replayed by
`drawing_critique` at finalize time.
## `drawing_critique(focus=None)`
Returns a list of `Issue` instances. **Empty list = pass.**
```yaml
Issue:
severity: error | warning | info
focus: <CritiqueFocus enum>
message: str
handles: list[str] # entities involved
detail: dict # numeric context
```
`CritiqueFocus` is a **closed enum** — adding a new focus requires a code change
to `plan_spec.py` AND `critique.py`. This is intentional: it keeps the critique
scope from drifting.
| Focus | Checks |
|-------|--------|
| `iso128` | Layer lineweights are in {0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0, 1.4, 2.0} mm |
| `layer_color` | Engineering layers keep their canonical ACI color |
| `dim_overlap` | No two dimensions within 5 mm of each other |
| `untrimmed_corner` | No two LINE endpoints within 0.5 mm but not exactly equal |
| `duplicate_entities` | No two LINEs with identical (or reversed) endpoints |
| `construction_left` | CONSTRUCTION layer is empty |
`focus=None` runs all six. Pass a list (e.g. `["construction_left", "untrimmed_corner"]`)
to scope a check during iterative editing.
## `point_from_snap(handle, snap, ref_x, ref_y)`
Eliminates LLM coordinate guessing. Returns `(x, y)` tuple.
| `snap` | LINE | CIRCLE/ARC | Required |
|--------|------|-----------|----------|
| `end` | start or end (closer to ref) | arc start or end | — (uses ref if given) |
| `mid` | midpoint | arc midpoint | — |
| `center` | — | center | — |
| `quad` | — | nearest of E/N/W/S quadrants | — (uses ref if given) |
| `perp` | foot of perpendicular from ref | — | `ref_x`, `ref_y` |
| `near` | nearest point on segment (clamped) | nearest on circle/arc | `ref_x`, `ref_y` |
| `int` | (V2 — needs 2nd handle) | | |
## `construction_xline(x, y, angle_deg, layer="CONSTRUCTION")`
Creates an infinite construction line on a non-printing layer (color 250,
lightest weight). Use as scaffolding when laying out geometry.
`construction_clear(layer="CONSTRUCTION")` — bulk-deletes everything on the
layer. Returns `{"ok": True, "deleted": N}`. **Idempotent.**
## `drawing_apply_iso_layers(standard)`
| `standard` | Layer count | Use case |
|------------|-------------|----------|
| `mech` | 10 | Mechanical part / assembly drawings |
| `pid` | 15 | Process & Instrumentation Diagrams (ISO 10628) |
| `iso13567` | 10 | ISO 13567 layer-naming convention starter |
Idempotent — existing layers are not modified.
## `dimension_auto(handles, style, offset)`
V1 supports LINE entities only. Generates one or more dimensions using the
chosen style.
| `style` | Behaviour |
|---------|-----------|
| `chain` | One linear dim per handle, perpendicular-offset from each segment |
| `baseline` | All dims share the first segment's start point as baseline |
| `ordinate` | X and Y coordinate dims for each segment endpoint |
`offset` (mm) controls the dim-line distance from the geometry.
## `entity_select_smart(predicate)`
Predicate is a dict; **all keys optional, AND-ed**:
```yaml
predicate:
type: LINE | CIRCLE | ARC | ... # entity type filter
layer: <name> # exact match
near: [x, y, radius] # entity within radius of point
length_range: [min, max] # LINE/ARC only
color: <ACI int> # exact match
```
Returns a list of `EntityInfo`. Use this in lieu of memorising handles when
chaining `dimension_auto` or other operations.
@@ -0,0 +1,159 @@
# Premium Workflow — Full Examples
The 9-step workflow walked through end-to-end. Use these as templates.
## Template 1 — Mechanical part (L-bracket with rounded inner corner)
```python
# 1. PLAN
plan = await drawing_plan(
intent="L-bracket 50x80, R5 inner corner, 5mm thick",
sheet_size="A4",
scale=1.0,
layer_set_id="mech",
dim_style="chain",
)
# 2. LAYERS
await drawing_apply_iso_layers("mech")
# 3. CONSTRUCTION (optional — set up reference grid)
await construction_xline(0, 0, 0) # horizontal axis
await construction_xline(0, 0, 90) # vertical axis
# 4. GEOMETRY (on GEOMETRY layer)
l1 = await entity_create_line(0, 0, 100, 0, layer="GEOMETRY")
l2 = await entity_create_line(100, 0, 100, 80, layer="GEOMETRY")
# 5. CORNERS
arc = await entity_fillet(l1["handle"], l2["handle"], radius=5)
# 6. SELECT for dimensioning (avoid handle memorization)
geometry_lines = await entity_select_smart({
"type": "LINE",
"layer": "GEOMETRY",
})
# 7. DIMENSIONS
dims = await dimension_auto(
[e["handle"] for e in geometry_lines],
style="chain",
offset=12.0,
)
# 8. CRITIQUE
issues = await drawing_critique(focus=None)
assert issues == [], f"Premium gate failed: {issues}"
# 9. CLEAR + FINALIZE
await construction_clear()
await drawing_finalize(
save_path="C:/work/l-bracket.dwg",
screenshot_path="C:/work/l-bracket.png",
)
```
## Template 2 — P&ID flow control loop
```python
# 1. PLAN
plan = await drawing_plan(
intent="Flow control loop 101: pump → valve → vessel",
sheet_size="A3",
scale=1.0,
layer_set_id="pid",
dim_style="ordinate",
notes=["ISA-5.1 tag conventions", "All process lines on PROCESS-PIPING-MAIN"],
)
# 2. LAYERS — bootstraps PROCESS-PIPING-MAIN, INSTRUMENT-*, etc.
await drawing_apply_iso_layers("pid")
# 3. EQUIPMENT (vessels, pumps — use blocks ideally)
await entity_create_circle(50, 50, 8, layer="PROCESS-EQUIPMENT") # pump body
await entity_create_rectangle(150, 30, 200, 70, layer="PROCESS-EQUIPMENT") # vessel
# 4. PROCESS LINES
main = await entity_create_line(58, 50, 150, 50, layer="PROCESS-PIPING-MAIN")
# 5. INSTRUMENT BUBBLES (circle + tag text)
ft = await entity_create_circle(100, 70, 5, layer="INSTRUMENT-SYMBOL")
await entity_create_text("FT-101", 95, 68, height=2.5, layer="INSTRUMENT-TAG-TEXT")
# 6. SIGNAL LINE (instrument bubble → controller)
await entity_create_line(100, 75, 100, 90, layer="INSTRUMENT-LINE-SIGNAL")
# 7. (no dimensions on P&ID typically — skip)
# 8. CRITIQUE — focus on what matters for P&ID
issues = await drawing_critique(focus=["layer_color", "iso128", "construction_left"])
assert issues == []
# 9. FINALIZE
await drawing_finalize(save_path="C:/work/loop-101.dwg")
```
## Template 3 — Iterative drawing (corrections welcome)
```python
# Initial plan
plan = await drawing_plan("Bracket draft", sheet_size="A4")
await drawing_apply_iso_layers("mech")
# Draw something wrong: leaves a gap
l1 = await entity_create_line(0, 0, 49.8, 0, layer="GEOMETRY")
l2 = await entity_create_line(50, 0, 50, 30, layer="GEOMETRY")
# Critique — flags untrimmed_corner
issues = await drawing_critique(focus=["untrimmed_corner"])
# issues[0].handles = [l1.handle, l2.handle]
# Fix in place: extend l1 to meet l2
await entity_extend(l1["handle"], l2["handle"])
# Re-critique
issues = await drawing_critique(focus=["untrimmed_corner"])
assert issues == []
```
## Common patterns
### Snap-driven coordinates
```python
# BAD: guessing
await entity_create_line(50, 50, 100, 50)
# GOOD: snap from existing geometry
mid = await point_from_snap(some_line["handle"], "mid")
end = await point_from_snap(other_line["handle"], "end", ref_x=100, ref_y=50)
await entity_create_line(mid["x"], mid["y"], end["x"], end["y"])
```
### Construction-first workflow
```python
# Lay out reference geometry
await construction_xline(0, 0, 0) # x-axis
await construction_xline(0, 0, 90) # y-axis
await construction_xline(100, 0, 90) # vertical at x=100
# Snap real geometry to construction intersections
ref = await point_from_snap(some_line["handle"], "perp", ref_x=0, ref_y=50)
# ... create real geometry on GEOMETRY layer ...
# Wipe scaffold before finalize
await construction_clear()
```
### Block-first repetition
```python
# When 12 holes are needed: create one block, then array
hole = await entity_create_circle(0, 0, 5, layer="GEOMETRY")
await block_create_from_entities("HOLE_M10", [hole["handle"]], 0, 0)
# Insert + array — far cheaper than 12 entity_create_circle calls
ref = await block_insert("HOLE_M10", x=0, y=0)
await entity_array_polar(ref["handle"], count=12, fill_angle=360, center_x=50, center_y=50)
```