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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e27071ab48
commit
8f405bddf3
@@ -0,0 +1,164 @@
|
||||
"""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)))
|
||||
Reference in New Issue
Block a user