feat(mech): SECTION 23 completes with centre_marks, section_line, hatch_material
centre_marks draws ISO 128-23 centre marks or full centre lines and reads
each circle's real centre and radius back out of the drawing through
entity_get instead of being told them - a radius restated from memory is
the silent wrong number this repository keeps refusing to ship. A handle
that is not a CIRCLE or ARC, and a circle in a plane tilted out of WCS XY
(which has no radius in this frame), are both refused by handle before
anything is written. The dash proportions are not transcribed here: the
prims are plain lines on CENTER, whose linetype already carries them.
section_line draws the ISO 128-40 cutting-plane line - wide segments at
the ends on GEOMETRY, the thin long-dash-dot middle on CENTER, an arrow at
each end whose head sits on the line end and points the way the section is
viewed, the same letter beyond both arrows - and returns the five-key
plane dict {p1, p2, label, direction, style} that mech_view_add(kind=
"section") consumes, so the label on the plan and the label on the view
are the same string by construction. A viewing direction parallel to the
plane is refused rather than drawn.
hatch_material hatches through standards.materials.hatch_for, so the
ISO 128-50 map and its refusal stay in one place; scale multiplies the
material's own pattern scale and angle replaces its angle. An unknown
material is refused with the list before any entity is written.
DEVIATION - one defect in the plan's own draw_material_hatch, fixed here
with two tests that fail against the plan's code. It called
entity_create_hatch with ROLE_LAYER["hatch"] without creating that layer,
the same omission review found in Task 10's draw_annotation_prims.
MEASURED live on AutoCAD 2026 (probe: GetActiveObject, Documents.Add(),
doc.Close(False) in a finally): on a scratch document whose layer table is
['0'], msp.AddHatch(0, "ANSI31", False) + AppendOuterLoop + Evaluate()
leaves the AcDbHatch in model space, and the very next statement
hatch.Layer = "HATCH" - which is the one line _apply_entity_attrs runs -
raises com_error (-2147352567, ..., 'Key not found', ..., -2145386476);
model space then still reports [('AcDbHatch', '0')] and the layer table is
still ['0'], so the refusal leaves an orphan hatch on layer 0. After
doc.Layers.Add("HATCH") the same assignment succeeds. drawing_open runs no
bootstrap, which is exactly when a cut face has no HATCH layer to land on.
draw_material_hatch now calls ensure_annotation_layers before creating the
hatch and reports layer / layers_created, as draw_annotation_prims does.
The whole task re-measured live afterwards through ComBackend on a fresh
scratch document (layer table ['0']): hatch_material -> {'ok': True,
'handle': '82', 'pattern': 'ANSI31', 'layer': 'HATCH', 'layers_created':
['HATCH']} with the entity read back as HATCH on HATCH; centre_marks on a
live CIRCLE r=12.5 -> sources [{'handle': '84', 'center': [80.0, 0.0],
'radius': 12.5}]; section_line -> 9 entities and the five-key plane; layer
table after ['0', 'CENTER', 'GEOMETRY', 'HATCH', 'TEXT']; document closed.
tests/test_mech_marks.py: 17 passed. Gates: ruff check clean, inventory
regenerated to 209 tools, test_tool_registry (engineering 12 -> 15, total
206 -> 209), test_discovery_aliases, test_tool_inventory,
test_capability_contract and test_release_consistency all pass. Full
suite: 3343 passed, the 6 pre-existing failures verified identical with
this change stashed, and
test_release_consistency.py::test_document_numbers_match_the_tree
deselected as Global Constraints instructs.
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
c607e75b75
commit
64a57d74a0
@@ -8124,7 +8124,7 @@ async def drawing_properties_set(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ── SECTION 23: Mechanical Annotation (2 tools) ─────────────────────────────
|
||||
# ── SECTION 23: Mechanical Annotation (5 tools) ─────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
# ISO annotation symbols composed from LINE / ARC / CIRCLE / LWPOLYLINE / TEXT,
|
||||
# so the same symbol lands on COM and ezdxf. The standards tables live in
|
||||
@@ -8304,6 +8304,166 @@ async def weld_symbol(
|
||||
)
|
||||
|
||||
|
||||
@cad_tool(
|
||||
summary="Draw ISO 128-23 centre marks or centre lines on circles, read from the drawing.",
|
||||
cost="mutate",
|
||||
)
|
||||
@mcp.tool(
|
||||
annotations={"title": "Centre Marks (ISO 128-23)", "destructiveHint": False},
|
||||
tags={"engineering", "mech"},
|
||||
)
|
||||
async def centre_marks(
|
||||
handles: Annotated[
|
||||
list[str] | None,
|
||||
"Handles of CIRCLEs/ARCs; their real centre and radius are read from the drawing.",
|
||||
] = None,
|
||||
centers: Annotated[
|
||||
list[list[float]] | None,
|
||||
"Explicit centres as [x, y] or [x, y, radius]; style='lines' needs the radius.",
|
||||
] = None,
|
||||
style: Annotated[
|
||||
str,
|
||||
Field(
|
||||
default="mark",
|
||||
description="mark (short cross at the centre) | lines (full centre lines).",
|
||||
),
|
||||
] = "mark",
|
||||
extension: Annotated[
|
||||
float,
|
||||
Field(
|
||||
default=3.0,
|
||||
gt=0,
|
||||
description="Arm length (mark) or overrun past the outline (lines), mm.",
|
||||
),
|
||||
] = 3.0,
|
||||
layer: Annotated[str | None, "Override the CENTER role layer."] = None,
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""Centre marks on the CENTER layer, whose linetype carries ISO 128-23's dash pattern.
|
||||
|
||||
Pass `handles` and the radii are read back with `entity_get` rather than
|
||||
restated — a radius from memory is the classic silent wrong number.
|
||||
Refusals, all before the first entity is written: neither `handles` nor
|
||||
`centers` given (or both), a handle that is not a CIRCLE or ARC, a circle
|
||||
in a plane tilted out of WCS XY (capability `ocs_tilted_plane`, so it has
|
||||
no radius in this frame), a `style` outside mark/lines, a non-positive
|
||||
`extension`, and `style='lines'` on a centre given without a radius.
|
||||
"""
|
||||
from engineering.mech.marks import draw_centre_marks
|
||||
|
||||
await ctx.debug(f"Centre marks style={style}")
|
||||
return await draw_centre_marks(
|
||||
_backend(ctx),
|
||||
handles=handles,
|
||||
centers=centers,
|
||||
style=style,
|
||||
extension=extension,
|
||||
layer=layer,
|
||||
)
|
||||
|
||||
|
||||
@cad_tool(
|
||||
summary="Draw an ISO 128-40 cutting-plane line and return the plane the section view uses.",
|
||||
cost="mutate",
|
||||
)
|
||||
@mcp.tool(
|
||||
annotations={"title": "Section Line (ISO 128-40)", "destructiveHint": False},
|
||||
tags={"engineering", "mech"},
|
||||
)
|
||||
async def section_line(
|
||||
x1: Annotated[float, "Cutting plane start X (WCS)."],
|
||||
y1: Annotated[float, "Cutting plane start Y (WCS)."],
|
||||
x2: Annotated[float, "Cutting plane end X (WCS)."],
|
||||
y2: Annotated[float, "Cutting plane end Y (WCS)."],
|
||||
label: Annotated[str, "Capital letter drawn at both ends, e.g. 'A'."] = "A",
|
||||
direction: Annotated[
|
||||
list[float] | None,
|
||||
"Viewing direction as [dx, dy]; must not be parallel to the cutting plane.",
|
||||
] = None,
|
||||
style: Annotated[
|
||||
str, "full | half | offset | revolved — carried into the returned plane."
|
||||
] = "full",
|
||||
height: Annotated[float, Field(default=5.0, gt=0, description="Label height (mm).")] = 5.0,
|
||||
layer: Annotated[str | None, "Override the role layers (GEOMETRY/CENTER/TEXT)."] = None,
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""Draw the cutting-plane line and hand back the plane a section view consumes.
|
||||
|
||||
Wide segments at the ends on GEOMETRY, the thin long-dash-dot middle on
|
||||
CENTER, an arrow at each end whose head sits on the line end and points the
|
||||
way the section is viewed, and the same letter beyond both arrows.
|
||||
`payload["plane"]` is exactly `{p1, p2, label, direction, style}` — feed it
|
||||
to the section view so the label on the plan and the label on the view
|
||||
cannot disagree. Refusals, before any entity is written: `p1 == p2`, a zero
|
||||
or parallel viewing direction, an empty `label`, a `style` outside
|
||||
full/half/offset/revolved, and a non-positive `height`.
|
||||
"""
|
||||
from engineering.mech.marks import draw_section_line
|
||||
|
||||
await ctx.debug(f"Section line {label} ({x1},{y1})-({x2},{y2})")
|
||||
return await draw_section_line(
|
||||
_backend(ctx),
|
||||
(x1, y1),
|
||||
(x2, y2),
|
||||
label=label,
|
||||
direction=tuple(direction) if direction else (0.0, -1.0),
|
||||
style=style,
|
||||
height=height,
|
||||
layer=layer,
|
||||
)
|
||||
|
||||
|
||||
@cad_tool(
|
||||
summary="Hatch a region with the ISO 128-50 section pattern for a named material.",
|
||||
cost="mutate",
|
||||
)
|
||||
@mcp.tool(
|
||||
annotations={"title": "Material Hatch (ISO 128-50)", "destructiveHint": False},
|
||||
tags={"engineering", "mech"},
|
||||
)
|
||||
async def hatch_material(
|
||||
material: Annotated[str, "Material name; an unknown one is refused with the list."] = "steel",
|
||||
boundary: Annotated[
|
||||
list[list[float]] | None, "Closed boundary as [[x, y], ...] — at least three points."
|
||||
] = None,
|
||||
handles: Annotated[
|
||||
list[str] | None, "Entities to chain into one closed loop instead of a boundary."
|
||||
] = None,
|
||||
scale: Annotated[
|
||||
float,
|
||||
Field(default=1.0, gt=0, description="Multiplies the material's own pattern scale."),
|
||||
] = 1.0,
|
||||
angle: Annotated[
|
||||
float | None, "Replaces the material's own pattern angle (degrees) when given."
|
||||
] = None,
|
||||
layer: Annotated[str | None, "Override the HATCH role layer."] = None,
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""Section-hatch a cut face with the pattern ISO 128-50 gives its material.
|
||||
|
||||
Refusals: an unknown material (named, with the full list, before anything
|
||||
is drawn), neither `boundary` nor `handles` (or both), a boundary with
|
||||
fewer than three points, and a non-positive `scale`. With `handles` the
|
||||
loop is chained by `boundary_from_entities`, which the COM engine refuses
|
||||
(capability `boundary_trace`) — pass `boundary` on a live seat, or run
|
||||
headlessly. The HATCH layer is created first when the drawing lacks it:
|
||||
ActiveX refuses an entity on an absent layer and would otherwise leave the
|
||||
hatch orphaned on layer 0.
|
||||
"""
|
||||
from engineering.mech.marks import draw_material_hatch
|
||||
|
||||
await ctx.debug(f"Material hatch {material}")
|
||||
return await draw_material_hatch(
|
||||
_backend(ctx),
|
||||
boundary=boundary,
|
||||
handles=handles,
|
||||
material=material,
|
||||
scale=scale,
|
||||
angle=angle,
|
||||
layer=layer,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ── RESOURCES ───────────────────────────────────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user