Files
autocad-mcp-pro/scripts/render_readme_showcase.py
T
Umut CANandClaude Opus 5 8f405bddf3 fix(dimensions): diameter and radius callouts measured the leader as geometry
`add_diameter_dim` / `add_radius_dim` measure to the point they are handed;
`leader_length` is a text placement. The ezdxf backend passed
`centre + (radius + leader)` as that point, so the leader became part of the
measurement: a true 40 mm bore was dimensioned 60 at the default
`leader_length=10`, and 108 at 34. Radius callouts were `leader_length` too
large the same way.

This is the worst class of defect for this project -- a wrong number, at
default settings, on the one part of a drawing that exists to carry numbers --
and it shipped in v1.4.0 and earlier. The size now comes from `radius` and
`location` only places the text.

The live COM backend was never affected: ActiveX's AddDimDiametric takes the
two chord points and the leader length as separate arguments. The two engines
had been disagreeing by 2 x leader_length on the same call.

Found while building the drawing for the new README, whose own bore callout was
wrong. Two checks added to correctness_suite.py; both report fail -> pass
against v1.4.0, not miss -> pass, which is what separates a repaired defect
from new capability in that lane. A/B is now 21/26 -> 26/26, five fixed, zero
regressed.

README rewritten from scratch
- Hero is a real ISO A3 sheet the server drew: m6 z24 involute spur gear, DIN
  6885 keyed bore, section A-A, ISO 129 dimensions, ISO 286 H7 fit, ISO 7200
  title block, 56 entities, drawing_critique clean. New
  scripts/render_readme_showcase.py rebuilds it, and the tool-call sequence
  that produces it is printed in the page.
- Star history chart, badge row, capability matrix, five evidence lanes, and a
  "what this release is bad at" section carrying the 28% throughput regression.
- Every image and repo link is now an absolute URL: README.md is the PyPI long
  description, where relative paths 404.

Corrections this work forced, all measured
- The df=0 example was wrong. FILLET is already in entity_fillet's own text;
  the commands that genuinely scored zero are WBLOCK, PEDIT, OVERKILL,
  LAYTRANS and MATCHPROP.
- 40,305 tokens is the tools/list wire payload; ~25,143 is the part forwarded
  to the model. The old wording said "of schema", which is neither.
- `--tokenizer anthropic` needs a package and credentials this repo does not
  lock, and refuses rather than falling back. Said so.
- Discovery ranks over the 149-tool advertised catalog, not the 154 registered.
- Five middlewares, not four; the fifth is what makes refusals machine-readable.
- Five of the 27 capability keys are environment-dependent, so the matrix
  carries footnotes instead of bare ticks.
- "Every coordinate is WCS" has one deliberate exception: TEXT rotation.
- render_live_chart's axis still said "fixed 10-task matrix" while our bar came
  from v3's fifteen. Each bar now carries its own matrix.
- .env.example said MAX_UNDO_STACK=50 (code: 5) and omitted four real
  variables, two of them the remote-HTTP controls.
- server.json described an empty ALLOWED_PATHS as blocking access. It disables
  the allowlist.
- server.py called the lean profile ~46 tools; it is 47.
- backends/capability.py cited a test file that does not exist.

1249 passed, 1 xfailed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 11:24:44 +03:00

165 lines
5.3 KiB
Python

"""Render the drawing the README shows, using only the server's own tools.
A CAD README that shows nothing but bar charts is asking to be taken on faith.
This produces the one asset that is not a claim: an actual sheet, built through
the same backend methods the MCP tools call, in the order CLAUDE.md's premium
workflow prescribes -- ISO layers, deterministic gear geometry, a keyed bore,
ISO 129 dimensions, an ISO 7200 title block -- and rendered headlessly.
Reproduce:
AUTOCAD_MCP_BACKEND=ezdxf python scripts/render_readme_showcase.py
Nothing here is decoration: every line on the sheet comes from a tool the
server exposes, so if a tool regresses the picture changes with it.
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
DEFAULT_OUTPUT = ROOT / "docs" / "assets" / "autocad-mcp-showcase.png"
#: A 24-tooth, module-6 spur gear. Sized to fill the A3 drawing area, because
#: the render crops to the sheet border: geometry that fills the border gets
#: the pixels, and involute flanks that do not resolve prove nothing.
GEAR_MODULE = 6.0
GEAR_TEETH = 24
BORE_DIAMETER = 40.0
KEYWAY_WIDTH = 12.0
KEYWAY_DEPTH = 3.3
FACE_WIDTH = 46.0
GEAR_CENTER = (135.0, 172.0)
SECTION_X = 300.0
#: No DIMSCALE here on purpose. `drawing_settings({"dimscale": ...})` sets the
#: header variable and reports success, but ezdxf renders dimensions from the
#: dimstyle table entry, so headlessly it changes nothing (measured). Setting it
#: in this script would put a call in the README's own showcase that does not do
#: what it looks like it does.
async def build(backend) -> dict:
from engineering.gear import draw_gear_section_aa, draw_spur_gear_front_view
from engineering.titleblock import TitleBlockMetadata, apply_iso_a3_titleblock
await backend.drawing_new()
await backend.drawing_apply_iso_layers("mech")
await backend.drawing_settings({"units": "mm", "linear_precision": 2})
gear = await draw_spur_gear_front_view(
backend,
module=GEAR_MODULE,
teeth=GEAR_TEETH,
center=GEAR_CENTER,
bore_diameter=BORE_DIAMETER,
keyway_width=KEYWAY_WIDTH,
keyway_depth=KEYWAY_DEPTH,
)
# `gear_metadata` is the geometry record, not the handle map the front view
# returns -- the section needs radii, not handles.
await draw_gear_section_aa(
backend,
gear_metadata=gear["metadata"],
x_offset=SECTION_X,
face_width=FACE_WIDTH,
)
# ISO 129 dimensions, and a real ISO 286 fit on the bore -- "H7" is the
# whole reason a drafter trusts a generated hole. `fit=` is resolved by the
# tool layer, so the deviations are looked up here the same way it does
# rather than typed in as text.
from engineering.fits import fit_lookup
cx, cy = GEAR_CENTER
pitch_radius = GEAR_MODULE * GEAR_TEETH / 2.0
outer_radius = pitch_radius + GEAR_MODULE
await backend.dimension_linear(
cx - pitch_radius,
cy,
cx + pitch_radius,
cy,
cx,
cy - outer_radius - 22.0,
layer="DIM",
)
# dimension_diameter takes the two ENDS OF A DIAMETER, not the centre and a
# point on the circle. Passing the latter halves the measured size, which is
# how the first draft of this sheet ended up calling a 40 mm bore 20.
bore_fit = fit_lookup("H7", BORE_DIAMETER)
await backend.dimension_diameter(
cx - BORE_DIAMETER / 2.0,
cy,
cx + BORE_DIAMETER / 2.0,
cy,
leader_length=34.0,
layer="DIM",
tol_upper=bore_fit.upper_mm,
tol_lower=-bore_fit.lower_mm,
tol_mode="deviation",
text_override=f"<> {bore_fit.code}",
)
await apply_iso_a3_titleblock(
backend,
metadata=TitleBlockMetadata(
title=f"SPUR GEAR m{GEAR_MODULE:g} z{GEAR_TEETH}",
drawing_no="AMC-1500-001",
part_no="1500-001",
material="C45E",
scale="1:1",
drawn_by="autocad-mcp-pro",
checked_by="drawing_critique",
date="2026-08-06",
revision="A",
),
origin=(0.0, 0.0),
)
issues = await backend.drawing_critique(focus=None)
info = await backend.drawing_info()
return {"issues": len(issues), "entities": info.entity_count, "gear": gear}
async def main(output: Path) -> int:
os.environ.setdefault("AUTOCAD_MCP_BACKEND", "ezdxf")
from backends.ezdxf_backend import EzdxfBackend
backend = EzdxfBackend()
await backend.connect()
try:
summary = await build(backend)
png = await backend.view_screenshot(overlay_handles=False)
finally:
await backend.disconnect()
if not png:
print("render produced no image", file=sys.stderr)
return 1
output.parent.mkdir(parents=True, exist_ok=True)
output.write_bytes(png)
print(
f"wrote {output} ({len(png)} bytes) - "
f"{summary['entities']} entities, {summary['issues']} critique issues"
)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
return parser
if __name__ == "__main__":
raise SystemExit(asyncio.run(main(build_parser().parse_args().output)))