feat(task-9): SECTION 22 - std_part_list, std_part_insert, std_feature_draw
engineering/mech/stdparts.py turns the Task 8 tables into real blocks. catalogue() searches 83 transcribed parts by designation, standard, size or family; block_spec() emits the typed primitives and DESIG/STD/SIZE/MAT ATTDEFs block_define takes, in side/top for fasteners and ISO 8826-1 simplified / ISO 8826-2 detailed for bearings; insert_std_part() defines the block once, inserts it and writes the std_part ACADMCP_MECH payload the skeleton pins, so the parts list is a read of the drawing rather than a naming convention. Threads follow ISO 6410 - minor at 0.8 x major, a thread-length limit line to the major, a three-quarter minor arc in the end view. std_feature_draw() adds a thread, a DIN 509 undercut, a DIN 471/472 groove, a DIN 332 centre hole or an ISO 3601-2 O-ring groove to geometry that already exists. Its dimensions come from the shared standards registry, never from this module: an absent table, a size outside coverage or a row missing a dimension is refused by name, and the DIN 509 relief is drawn from the profile its table publishes rather than reconstructed here. DIN 332 form R is refused outright. An unknown designation is refused with the nearest catalogue entries; nothing is drawn approximately. SECTION 22 registers the three tools in the new mech pack, with alias records (INSERT plus shop-floor synonyms in English and Turkish), a regenerated tool inventory, the README release snapshot at 207 tools in 24 groups, and the frozen group-size snapshot gaining mech: 3. Two plan corrections, both verified by running the code: * the plan's @cad_tool summaries for std_part_insert (118 chars) and std_feature_draw (130) overran discovery.serialize.MAX_SUMMARY_CHARS = 110, so tests/test_tool_registry.py::test_no_authored_summary_is_long_enough_to_be_truncated failed - they would have shipped rendered as "...". Both are shortened to 99 and 104 characters with the same content. * the plan's file list omitted tests/test_tool_packs.py, which pins the pack registry, the tool_packs report and the group count. It now carries MECH_TOOLS and distinguishes ALL_PACKS (declaration order, what "available" reports) from ALL_PACKS_ENABLED (sorted, what "enabled" reports) - those two lists stopped being the same list when "mech" was appended after "settings". No new ActiveX member is called: the module drives block_list / block_define / block_insert / entity_set_xdata / entity_create_* only. The COM path was still executed live once on AutoCAD 2026 in a scratch document created and closed by the probe: the bolt block defined with layers CENTER+GEOMETRY, attributes read back as DESIG/STD/SIZE/MAT = ISO 4014 - M12x60 / ISO 4014 / M12x60 / 8.8, the ACADMCP_MECH std_part payload round-tripped through entity_get_xdata, STD_ISO_15_6205_SIMPLIFIED inserted, and the M20 thread's three primitives landing on GEOMETRY. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
6024723d6b
commit
84c1ef42fb
@@ -690,6 +690,7 @@ async def _registered_tool_count() -> int | None:
|
||||
# entity/drawing tags. Tools whose tags match none fall into "other".
|
||||
_GROUP_TAG_PRIORITY = (
|
||||
"pid",
|
||||
"mech",
|
||||
"style",
|
||||
"pagesetup",
|
||||
"environment",
|
||||
@@ -718,6 +719,7 @@ _GROUP_TAG_PRIORITY = (
|
||||
# Map the winning tag to a human-readable group label for the breakdown.
|
||||
_GROUP_TAG_LABELS = {
|
||||
"pid": "pid",
|
||||
"mech": "mech",
|
||||
"style": "styles",
|
||||
"pagesetup": "page_setup",
|
||||
"environment": "environment",
|
||||
@@ -960,7 +962,7 @@ SOLID_TOOL_NAMES = frozenset(
|
||||
# mechanically should not pay for the P&ID surface, and a client that never
|
||||
# talks to a live seat should not pay for the environment surface. Styles and
|
||||
# page setup (SECTION 18/19) are drafting essentials and stay in core.
|
||||
TOOL_PACK_NAMES = ("core", "pid", "settings")
|
||||
TOOL_PACK_NAMES = ("core", "pid", "settings", "mech")
|
||||
PACK_TOOL_NAMES: dict[str, frozenset[str]] = {
|
||||
"pid": frozenset(
|
||||
{
|
||||
@@ -1002,6 +1004,14 @@ PACK_TOOL_NAMES: dict[str, frozenset[str]] = {
|
||||
"system_prompt_message",
|
||||
}
|
||||
),
|
||||
# SECTION 22 - the standard-parts catalogue (tracks B+G).
|
||||
"mech": frozenset(
|
||||
{
|
||||
"std_part_list",
|
||||
"std_part_insert",
|
||||
"std_feature_draw",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
_active_tool_profile: dict | None = None
|
||||
@@ -8123,6 +8133,197 @@ async def drawing_properties_set(
|
||||
return await _backend(ctx).drawing_properties_set(summary, custom)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ── SECTION 22: Standard Parts (3 tools) ────────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@cad_tool(
|
||||
summary="Search the standard-parts catalogue: ISO bolts, nuts, washers, cap screws, bearings.",
|
||||
cost="read",
|
||||
)
|
||||
@mcp.tool(
|
||||
annotations={"title": "Standard Parts: Catalogue", "readOnlyHint": True},
|
||||
tags={"mech", "query"},
|
||||
)
|
||||
async def std_part_list(
|
||||
query: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Free text matched against designation, standard, size and family - "
|
||||
"'M12', 'ISO 4014', '6205', 'washer'"
|
||||
),
|
||||
),
|
||||
] = None,
|
||||
standard: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
default=None,
|
||||
description="One of ISO 4014, ISO 4017, ISO 4032, ISO 7089, ISO 4762, ISO 15",
|
||||
),
|
||||
] = None,
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""Catalogue rows with the dimensions a drawing needs, and their provenance.
|
||||
|
||||
Every row is transcribed from the named standard's own table and carries
|
||||
its `source`. Coverage is deliberately narrow: ISO 261 first-choice sizes
|
||||
only (M5-M36, M3-M24 for ISO 4762) and ISO 15 bore numbers 00-10 in the
|
||||
600x / 620x / 630x series. A size outside a table is refused by name by
|
||||
`std_part_insert` with the nearest entries, never interpolated. An unknown
|
||||
`standard` is refused with the list of catalogue standards.
|
||||
"""
|
||||
from engineering.mech.stdparts import FAMILY_STANDARDS, catalogue
|
||||
|
||||
rows = catalogue(query, standard)
|
||||
return {"count": len(rows), "standards": list(FAMILY_STANDARDS), "parts": list(rows)}
|
||||
|
||||
|
||||
@cad_tool(
|
||||
summary=(
|
||||
"Insert an ISO standard part - hex bolt, nut, washer, cap screw or ball bearing "
|
||||
"- as a tagged block."
|
||||
),
|
||||
cost="mutate",
|
||||
)
|
||||
@mcp.tool(
|
||||
annotations={"title": "Standard Parts: Insert", "readOnlyHint": False},
|
||||
tags={"mech", "create"},
|
||||
)
|
||||
async def std_part_insert(
|
||||
designation: Annotated[
|
||||
str,
|
||||
Field(
|
||||
description=(
|
||||
"'ISO 4014 - M12x60', 'ISO 4032 - M12', 'ISO 7089 - M12', "
|
||||
"'ISO 4762 - M8x30', 'ISO 15 - 6205' (a bare '6205' also works)"
|
||||
)
|
||||
),
|
||||
],
|
||||
x: Annotated[float, Field(description="Insertion X (WCS)")],
|
||||
y: Annotated[float, Field(description="Insertion Y (WCS)")],
|
||||
view: Annotated[
|
||||
str,
|
||||
Field(
|
||||
default="side",
|
||||
description=(
|
||||
"Fasteners: side | top. Bearings: simplified (ISO 8826-1) | detailed "
|
||||
"(ISO 8826-2). The default 'side' falls back to the family's first view."
|
||||
),
|
||||
),
|
||||
] = "side",
|
||||
rotation: Annotated[
|
||||
float,
|
||||
Field(default=0.0, description="Degrees CCW; 0 puts the part's axis along +X"),
|
||||
] = 0.0,
|
||||
layer: Annotated[
|
||||
str,
|
||||
Field(
|
||||
default="GEOMETRY",
|
||||
description=(
|
||||
"Layer for the INSERT; the block's own primitives keep GEOMETRY / CENTER / HIDDEN"
|
||||
),
|
||||
),
|
||||
] = "GEOMETRY",
|
||||
material: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
default=None,
|
||||
description="Property class or material for the MAT attribute and the XDATA, e.g. '8.8'",
|
||||
),
|
||||
] = None,
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""Define the part's block once, insert it, and write its `std_part` XDATA.
|
||||
|
||||
The block carries DESIG / STD / SIZE / MAT attributes and an ACADMCP_MECH
|
||||
payload, so a parts list is a read of the drawing rather than a naming
|
||||
convention. Refusals, all before any write: a designation outside the
|
||||
transcribed tables (named, with the nearest catalogue entries - nothing is
|
||||
drawn approximately), a malformed designation, a missing or stray nominal
|
||||
length, a view the family does not have, an attribute tag the block does
|
||||
not carry, and a non-finite coordinate or rotation.
|
||||
"""
|
||||
from engineering.mech.stdparts import insert_std_part
|
||||
|
||||
await ctx.info(f"standard part {designation} ({view}) at ({x}, {y})")
|
||||
return await insert_std_part(
|
||||
_backend(ctx),
|
||||
designation,
|
||||
at=(x, y),
|
||||
view=view,
|
||||
rotation=rotation,
|
||||
layer=layer,
|
||||
material=material,
|
||||
)
|
||||
|
||||
|
||||
@cad_tool(
|
||||
summary=(
|
||||
"Draw a standard feature - thread, undercut, ring or O-ring groove, centre hole "
|
||||
"- onto existing geometry."
|
||||
),
|
||||
cost="mutate",
|
||||
)
|
||||
@mcp.tool(
|
||||
annotations={"title": "Standard Parts: Draw Feature", "readOnlyHint": False},
|
||||
tags={"mech", "create"},
|
||||
)
|
||||
async def std_feature_draw(
|
||||
kind: Annotated[
|
||||
str,
|
||||
Field(description="thread | undercut | ring_groove | centre_hole | oring_groove"),
|
||||
],
|
||||
x: Annotated[float, Field(description="Feature origin X (WCS): a point on the axis")],
|
||||
y: Annotated[float, Field(description="Feature origin Y (WCS): a point on the axis")],
|
||||
params: Annotated[
|
||||
dict,
|
||||
Field(
|
||||
description=(
|
||||
"thread: {d, length, size|pitch, internal}; undercut: {d, form: E|F}; "
|
||||
"ring_groove: {d, kind: shaft|bore}; centre_hole: {size, form: A|B}; "
|
||||
"oring_groove: {d, cord, kind: shaft|bore}"
|
||||
)
|
||||
),
|
||||
],
|
||||
rotation: Annotated[
|
||||
float,
|
||||
Field(
|
||||
default=0.0,
|
||||
description="Degrees CCW applied to the local +X (along the axis, into the feature)",
|
||||
),
|
||||
] = 0.0,
|
||||
layer: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
default=None,
|
||||
description="Override the visible primitives' layer; CENTER and HIDDEN keep theirs",
|
||||
),
|
||||
] = None,
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""Add a standard feature to geometry that already exists - a foreign drawing being finished.
|
||||
|
||||
Local frame: `(x, y)` is on the axis, +X runs along the axis in the
|
||||
direction the feature extends and +Y is radially outward. The thread is the
|
||||
ISO 6410 representation (minor diameter at 0.8 x major, thread-length limit
|
||||
to the major) and its pitch comes from ISO 261 via `size` or from an
|
||||
explicit `pitch` - it is never guessed. Every other kind takes its
|
||||
dimensions from the registered standards table and is refused by name when
|
||||
that table is absent, when the size is outside its coverage, or when the row
|
||||
lacks a dimension the feature needs: no value is interpolated and no profile
|
||||
is reconstructed. DIN 332 form R is refused - only forms A and B are drawn.
|
||||
"""
|
||||
from engineering.mech.stdparts import draw_std_feature
|
||||
|
||||
await ctx.info(f"standard feature {kind} at ({x}, {y})")
|
||||
return await draw_std_feature(
|
||||
_backend(ctx), kind, (x, y), params, rotation=rotation, layer=layer
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ── RESOURCES ───────────────────────────────────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user