`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>
165 lines
5.3 KiB
Python
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)))
|