3248bfbfea50c5a9fe2c27db784fcc029b6796d6
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3248bfbfea |
docs(plan): track F skeleton — 10 tasks in three groups, interfaces pinned
Header, global constraints, file structure, the interface block every task builds against, the group/ownership table, the SECTION 25 banner protocol and the merge notes. Task sections are drafted next. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
83f192ac2f |
docs: track F (architectural 2D) design spec — the plan model
One plan model — a wall network with hosted openings — and one engine that resolves L/T/X junctions, cuts openings, hatches cut walls by material and computes rooms as the faces the walls enclose, so areas are measured, never typed. A pure planar-face finder reads rooms from foreign plans on both engines (the live engine has no boundary_trace). Furniture/sanitary catalogue, grid, symbols, exterior dimension chains, schedules from the model. 12 tools (SECTION 25, pack arch), English by default with lang="tr", poché by material, three arch_* critique focuses, benchmark arch_roundtrip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
2bba18d0f7 |
fix(com): hatch islands on a live seat — typed edges become temporary entities
hatch_add_boundary was refused on COM (hatch_edge_paths), so a transverse section of any hollow part — a bushing, a flange bore — could not be drawn in AutoCAD at all: draw_prims asked the capability map and refused up front. ActiveX takes a loop as whole objects, which is not the same as "cannot take a typed edge": each edge now becomes a real temporary LINE, true ARC or ELLIPSE in the hatch's own space, appended with AppendInnerLoop, evaluated and deleted. A loop that is only chained lines becomes one closed polyline — one COM call instead of 180 for a flattened bore. The edge list is validated in full first with the headless engine's messages; a loop AutoCAD refuses is taken back out with its temporaries. Measured on AutoCAD 2026 (25.1s), in scratch documents closed unsaved: - a two-arc island leaves exactly 10000 - 100*pi (9685.8407) of a 100 x 100 hatch; - the OD40/ID20 sleeve's transverse section B-B, drawn through the view engine and draw_prims, hatches 942.2864 mm2 — the engine's declared 180-gon; - an open chain is refused (E_FAIL) and the entity count is unchanged. hatch_edge_paths is now supported on both engines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
e6f685643d |
fix(com): a DXF type filter finds what ActiveX names differently
block_find_references asks entity_list(type_filter="INSERT"), and ActiveX calls a block reference AcDbBlockReference, so on a live seat the filter matched nothing and every block reported zero references. Measured on AutoCAD 2026 (25.1s): AcDbBlockReference, AcDbPolyline, and one class per dimension kind (AcDbRotatedDimension, AcDbAlignedDimension, AcDbDiametricDimension, AcDbRadialDimension, AcDb2LineAngularDimension, AcDbOrdinateDimension). entity_list and entity_count now let the DXF name find them: INSERT matches BLOCKREFERENCE/MINSERTBLOCK, LWPOLYLINE matches POLYLINE, and DIMENSION every *DIMENSION class. The reported type stays the ActiveX spelling, so a caller already filtering by it is unaffected. Verified live in a scratch document: two inserts, one dimension and one line are found by INSERT, DIMENSION and LINE (the INSERT filter found 0 before). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
898e775f93 |
bench: matrix v5 adds mech_assembly; the correctness suite grows with the mechanical and sheet checks
The evidence for tracks B and G, in the shape tracks A and E used.
- benchmarks/tasks_v5.py: 18 tasks, DEFAULT_MATRIX = "v5"; v4, v3 and v2 stay
addressable so an earlier report reproduces exactly rather than being only
described. mech_assembly is roadmap criterion 3 - a flange-coupling sheet
built from the part model and the sheet standard, through the code the
tools call (apply_titleblock, draw_part/add_view/dimension_part,
insert_std_part, extract_records -> draw_bom_table, add_balloon), gated on
two numbers that cannot be negotiated: drawing_critique(focus=None) returns
zero issues and the finalize score is at least 90 (it scores 100, 18/18 on
the v5 matrix). The adapter returns the focuses that fired, so a
regression names itself, and a test drops one balloon to prove the gate
fails and names mech_bom_balloon_mismatch.
- Seven correctness checks (32 -> 39). mech_section_hatch_area is the one
that cannot be faked: a sleeve of length 60, outside diameter 40 and bore
20 cuts two faces totalling 1200 mm^2 exactly, read back out of the drawing
through entity_measure. sheet_frame_iso5457_a3 pins the A3 frame (20,10 to
410,287 inside the 420 x 297 trimmed sheet); mech_iso286_on_dimension pins
40 H7 at +0.025/0 through the layout; std_part_iso4014_m12 pins s = 18 and
k = 7.5 by value; mech_thread_unrepresented_is_caught is the seventh: a
quality gate that never fires is not a gate.
- scripts/smoke_mech_com.py executed every new COM path once on AutoCAD 2026
(25.1s): frame, title block, a sectioned shaft with 12 dimensions, an ISO
4014 block with 1 ACADMCP_MECH chunk, a weld symbol, the parts list read off
the drawing, a linked balloon, an xref attached / listed / detached, a
31687-byte .dwg (AC1032) through ActiveX SaveAs, critique 0 issues, exit 0,
every scratch document closed. It retries RPC_E_CALL_REJECTED (measured:
the first call after Documents.Add is refused) and tracks its documents by
the document list, since SaveAs renames them.
- Defects the evidence found, each fixed with a test that fails on the old
code:
* ezdxf dimension_diameter put the text beyond the SECOND chord point;
ActiveX AddDimDiametric puts it beyond the first (measured live: a d50
from (x,170) to (x,220), leader 10, text at (x,160)). Headlessly every
front-view diameter stood on its own chain dimension. ezdxf now follows
ActiveX.
* draw_part's anchor goes on MECH; ActiveX refuses entity.Layer on a
missing layer (measured -0x7ffdfff7 'Key not found'). draw.py ensures
every target layer before the first entity.
* bom.extract_records filtered on INSERT; ActiveX names it BLOCKREFERENCE
(ObjectName AcDbBlockReference) - the live parts list was empty.
* COM _entity_info reported no dimension text_position: ModelSpace.Item
narrows to IAcadEntity, which has no TextPosition (measured
AttributeError); read through _com_unnarrow (TextPosition measured on
AcDbDiametricDimension, WCS). dim_overlap compared bbox centres and a
diameter collided with its own bore.
* pid graph raised KeyError 'start' mid-critique on a LINE whose point read
AutoCAD refused while busy; such a line is left out.
* the validator read "ISO 4014 - M12x60" as fake dimension text.
- The published A/B report is re-run over the grown suite rather than
re-typed (26/39 -> 39/39, 13 fixed, 0 regressed), check_doc_numbers derives
the task count from the v5 matrix, the token table is remeasured with a
TOOL_PACKS=core,mech row (193 tools / 61,673 tokens), and the README's
collected-tests figure is retaken (3990).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
3b033033f9 |
feat(mech): six mech_* critique focuses, the mech pack, lean 60, aliases and the counts
The four track-B/G branches are merged; this is the integration that makes the 25 new tools one surface instead of four. - engineering/mech/critique.py: the six focuses of spec 13, dispatched the way the P&ID ones are. One index per critique run, shared through run_critique's context, so six focuses cost one read of the drawing; a drawing with no ACADMCP_MECH payload yields nothing at all, which is what keeps focus=None cheap for a P&ID sheet or a plain DXF. Each focus has a positive and a negative fixture. Two narrowings are in the module docstring rather than hidden: mech_view_misaligned checks the projection axis and not the quadrant, because the pinned payload carries no projection angle to check it against, and mech_duplicate_dimension catches two dimensions in the same place rather than a semantic restatement, because no engine exposes what a dimension measured. - The index reads what the tools actually write, not only the plan's fixture shape. mech_part_draw anchors its part payload on a POINT (layer MECH) and lists every drawn view under "views"; the plan's index read INSERT/CIRCLE only and one "view" per payload, so it was silent on every part the server draws. It now reads POINT too, expands "views" into one entry per drawn view (the section check looks in the section's own WCS box, the alignment check compares frame_bbox centres), and pages entity_list instead of one call. Test: a part drawn by draw_part + add_view is read and passes all six; the same part with its hatch erased is reported. - Live type names, measured on AutoCAD 2026 (2026-09-23, scratch document, closed): ObjectName AcDbBlockReference -> BLOCKREFERENCE, AcDbRotatedDimension -> ROTATEDDIMENSION, AcDbAlignedDimension -> ALIGNEDDIMENSION, AcDbPoint -> POINT with GetBoundingBox = the point. The index reads BLOCKREFERENCE as an INSERT and any *DIMENSION as a dimension; a wrapper test reports the ezdxf entities under exactly those measured names. - mech_thread_unrepresented uses the view engine's own MINOR_RATIO, and checks the thin line ISO 6410 draws: the minor diameter of an external thread, the major of an internal one. - mech_bom_balloon_mismatch: identical parts share one balloon (ISO 6433 balloons a parts-list row), so a second M12x60 is covered by the first one's balloon instead of reported. - TOOL_PACK_NAMES has "mech" (the 14 tools of SECTIONS 21-23). SECTION 24 stays core: a frame, a title block, a parts list and an xref are universal drafting, not a vertical. Lean gains five and reaches 60 (54 with TOOL_PACKS=core). - _GROUP_TAG_PRIORITY gains sheet directly after mech; the mech group label becomes "mechanical". SECTION 24's last four tools (xref_attach, xref_manage, image_attach, drawing_export_dwg) now carry the sheet tag, so group, pack and SECTION are one list: 229 tools in 25 groups, and engineering/blocks/entity_creation/drawing return to their pre-SECTION-24 sizes. - Ten golden tuning cases, two of them Turkish (with the diacritic spellings added to the alias records next to the ASCII-folded ones). The shared AutoCAD command set needed no change: the merge had already resolved TABLE, REVCLOUD, SECTIONPLANE, INSERT, ARRAY and SAVEAS, and nothing is stale. - Inventory regenerated, the frozen group-size snapshot retaken from the live registry, and the README release-snapshot / pack / lean figures taken from scripts/check_doc_numbers.py rather than typed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
a6c9c0d927 |
merge: tracks B+G group G
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
76e8647e1b |
merge: tracks B+G group A
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
36dab4cec6 |
merge: tracks B+G group S
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
5b70533505 |
merge: tracks B+G group M
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
5388440c27 |
fix(task-7): rotation honoured end to end, each view dimensioned once, refusals that name their path
- rotation turns the whole sheet of views about `at` as one rigid body: the
prims, the recorded WCS `bbox` (plus the sheet-frame `frame_bbox` the next
view is laid out against), the detail marker, and every dimension intent
(points, text position, and the linear measuring direction + rotation).
Measured headless: rotation=90 at (100,100) now draws (70,100)-(100,172),
records exactly that box, and leaves only the 2 diameter chord defpoints
off-geometry (was 4, up to 47.5 mm away).
- dimension_part dimensions only views whose record is not `dimensioned`,
takes `views=[indices]`, refuses a view already dimensioned by name
(ISO 129-1), writes the flag back, places the hole table once, and refuses
a fit no target view claims before drawing anything. A view the dimension
layer emits nothing for (a detail) is reported in `skipped` and left
unflagged. mech_dimension_part gains the `views` argument.
- draw_prims checks hatch_add_boundary's {"ok": False}: the hatch is removed
and the failure raised, never left solid through the bore. An engine whose
capability map says `hatch_edge_paths` is unsupported (the live one) is
refused before any prim of the view is drawn.
- a view scale other than 1.0 is refused by name in draw_part and add_view
(views are drawn full size; the scale would only have been recorded).
- add_view's collision step compares real rectangles on both axes and steps
along the projection axis layout_origin chose, so a third-angle view is
never shoved back onto the front view and a top view keeps its alignment.
- draw_from_spec validates `at` / `rotation` into the `parts[i]:` path and a
transaction that cannot open is a ValueError (a ToolError at the tool).
No new ActiveX member is called: the change composes existing contract
members (dimension_linear rotation, entity_delete, capabilities()).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
301610620c |
fix(task-14): detach an xref out of every space, and stop the live engine inventing facts
Three blocking findings on the refs group, all of them the same shape: a number or a refusal the caller was entitled to read as measured. 1. ezdxf xref_manage: the victim scan and the `inserts` count both walked `doc.modelspace()` only, then dropped the definition with `delete_block(safe=False)` -- the flag that suppresses the in-use check. An xref shown on a paper-space sheet was reported as 1 insert where the drawing held 2, and detaching it left that sheet INSERT with no BLOCK behind it: `ezdxf.recover` reports `FIX 103 UNDEFINED_BLOCK: Deleted entity INSERT(#34) without required BLOCK definition` and the repo's own drawing_audit silently deletes the sheet geometry. The scan now walks `doc.blocks`, which is every entity space (model space, each paper-space tab, every block definition) and is exactly what ezdxf's own safe check searches with `doc.query('INSERT[name==...]i')` -- hence the case-insensitive name match. `delete_block` is now `safe=True`, as the plan's Interfaces block pins it, so a future miss raises instead of writing a corrupt file. 2. COM xref_manage(action='list') reported `kind: "attach"` for every row, a literal constant. Re-measured on this seat's registered type library (C:\Program Files\Common Files\Autodesk Shared\acax25*.tlb, "AutoCAD 2025 Type Library", installed with AutoCAD 2026): IAcadBlock's xref members are exactly IsXRef, Path, Name, Reload, Unload, Bind, Detach, XRefDatabase -- no overlay indicator exists on the interface, so an overlay was reported to the client as an attachment. The row now says `kind: None`, the same honesty the adjacent `inserts` field already had, and the contract plus the tool docstring say so. 3. COM xref_attach had no duplicate-block-name guard, although contracts/refs.py and the tool docstring both promise the refusal and ezdxf implements it. Two different part.dwg files, or a re-attach after a repath, silently re-pointed or half-wrote the definition while the payload claimed the new path. Added `_block_names(doc)` (Blocks.Count / Blocks.Item(index) / block.Name -- the members xref_manage already walks) and the refusal fires before AttachExternalReference is dispatched, with the same message the headless engine gives. Lower-cased, because AutoCAD symbol-table names are case-insensitive. Tests: five new ones, each failing against the unfixed backends -- an xref shown on Layout1 is counted and detached with the drawing, the detached file audits clean through ezdxf.recover, delete_block's in-use check is pinned, the live listing reports kind None (against a fake that models only the measured members, so it cannot enshrine an invented one), and the live attach refuses a held name (xref and plain block, either case) without dispatching. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
8b14d4ef8d |
feat(task-7): SECTION 21 - the mechanical part model as six tools
engineering/mech/draw.py is the only module in the package that touches a backend: it turns the pure engine's primitive descriptions into entity_create_* and dimension_* calls, places them in WCS, and writes the part model back onto the drawing as ACADMCP_MECH XDATA on an anchor POINT. It is not a contract member and neither are the tools - exactly like gear_draw_* and titleblock_apply_iso_a3, it composes the generic members, so it is written once and works on both engines by construction and no capability key is needed. No new ActiveX call was written, so there was nothing to probe on the live seat; measured instead that every one of the 23 backend members draw.py calls exists on ComBackend and that ComBackend has no abstract methods left. mech_part_draw takes one model - a turned profile or a plate outline plus typed features - and draws its views in first-angle positions (third on request), dimensions them and records the placement. mech_view_add reads the model back out of the drawing and adds a side, top, section (full/half/offset/revolved) or detail view months later without the caller re-describing the part. mech_dimension_part lays out ISO 129 dimensions with no overlapping text, resolves ISO 286 fits against the measured nominal, and moves the holes into a table past a threshold. mech_hole_pattern exposes the same expansion for a drawing that was never built from the model - with its own diameter guard, because Hole.validate needs a part and a standalone pattern has none. mech_part_inspect reads the payload back and never modifies the drawing. mech_part_from_spec draws a whole sheet inside one transaction. Every part is validated before the transaction opens, so a refusal leaves the drawing untouched and names the path (parts[1]: segments[0].d_outer: ...), and the rollback is shielded with the shape engineering/pid/spec.py documents: anyio cancellation is level-triggered, so a bare await in the handler would be cancelled before the rollback body ran and leave half a sheet inside an open transaction. fits goes through dimension_intents(fits=...), the API Task 6 shipped, rather than being stamped on at draw time: that is the path that validates a key by name and refuses a fit on a reference dimension or on a hole that moved into the table. It is scoped per view first - a fit on segment[0] names nothing in the end view, and refusing there would make a correct three-view sheet undrawable - and any key no view claimed is handed back to dimension_intents so the refusal is written in its own vocabulary. SECTION 21 registers the six tools in the new mech pack, with alias records (SECTIONPLANE, ARRAY plus shop-floor synonyms in English and Turkish - mil, kademeli mil, kama, pah, kesit, gorunus), a regenerated tool inventory, the README release snapshot at 210 tools in 24 groups, and the frozen group-size snapshot gaining mech: 6. Lean is deliberately untouched here: spec section 11 puts three of these on the lean surface together with group G's two sheet tools, and Task 15 makes that edit once across all four branches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
bd73b3a854 |
fix(task-6): a reachable ISO 286 route, a tolerance schema that refuses, a reference dimension
Three review findings, all root causes in `engineering/mech/dimension.py`.
1. The ISO 286 fit path was unreachable from the module's own pipeline.
`resolve_tolerance` resolved `intent.fit` correctly, but nothing produced a
DimIntent carrying one: no feature and no segment has a fit, and
`dimension_intents` constructed every intent without `fit=`. Only a
hand-built DimIntent - which is what the tests used - reached the code, so
the spec's `mech_iso286_on_dimension` and the `mech_assembly` benchmark's
"ISO 286 fits on the bearing seats" had no route.
`dimension_intents(..., fits={key: code})` now stamps the code onto the
intent its key names, and the part's own diameters became addressable one
at a time through the new `segment_key(i, bore=...)` -> "segment[1]",
"segment[1].bore"; a feature's stay addressable by id. A key that names
nothing is refused with the names that exist, a malformed code is refused
where it is attached rather than at draw time, a fit on a reference
dimension or on one that already carries a tol is refused, and a fit on a
hole whose diameter moved into the hole table (which has no tolerance
column) is refused by name instead of vanishing. Task 7's planned
`dimension_part(..., fits=None)` passes straight through - the argument is
additive and keyword-only, so every call written against the pinned
signature keeps working.
The fit also now reaches the *text*: `intent_text` expands the `<>`
placeholder and appends the code, so `text_rects` measures the string that
is actually drawn and a hole that already names itself ("⌀6") keeps its own
text and gains the code.
2. An explicit tolerance was silently discarded when `tol` omitted "mode".
`mode = tol.get("mode", "none")` and `build_dim_override(0.05, 0.05,
"none", None)` returns `({}, None)` without raising, so a ±0.05 resolved,
echoed its own numbers back in the payload, and drew a dimension with no
DIMTOL/DIMTP/DIMTM at all. `DimIntent.tol` now has a schema - {"mode",
"upper", "lower"} and nothing else - validated by `_explicit_tol`: an
unknown key, a missing "mode", a non-dict, and deviations under a mode that
writes none are each refused by name. The mutual-exclusion guard reads
`tol is not None` rather than truthiness, so `tol={}` is refused too.
3. The ISO 129-1 "no measurement twice" test was vacuous and the property was
false. The old test built the key `_dedupe` uniquifies as the last step of
`dimension_intents`, so it could not fail; meanwhile `style="chain"` emitted
20, 40, 15 *and* 75 as four plain DIMENSIONs. `redundant_linear` now finds
the span that shorter ones tile end to end on the same axis and level, and
`dimension_intents` marks it as an auxiliary (reference) dimension - the new
`REFERENCE_FORMAT` "(<>)", which renders "(75)". The plan's own test still
holds (the 75 is present); it is no longer a fourth plain dimension. The
error is one-sided on purpose: a tiling that is found is real, a tiling the
greedy walk misses leaves the dimension plain, which is the direction the
repository's critique rule wants. Baseline and ordinate parenthesise
nothing, and two horizontal chains at different heights are not each other's
pieces.
Tests: the vacuous gate is replaced by four that fail against the pre-fix code
- `redundant_linear` on the shaft's plain intents returns (3,) there and ()
here; the parametrised "a tolerance is either refused or reaches the dimension
variables" gate resolves the mode-less shape to an empty DIMVAR override
there; and `fits=` is a TypeError there. `_dedupe` is now exercised through a
duplicate fed in via the view's own dims, which the collector does not
generate itself. 46 tests in tests/test_mech_dimension.py (was 23).
Live AutoCAD: no finding was measured live and no new ActiveX call was
written - this module is pure Python with no backend, no async and no DXF. A
DIMTOL probe against the running seat was attempted twice and
`GetActiveObject("AutoCAD.Application")` blocked past the ~90 s contention
window both times, so the live engine was unavailable; no scratch document was
created (the probe never got the application object) and none was left open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
3ef316d520 |
feat(refs): xrefs, raster underlays, data extract and real DWG output
SECTION 24 completes at eleven tools with xref_attach, xref_manage,
image_attach, data_extract and drawing_export_dwg, on the four contract members
that genuinely differ per engine -- backends/contracts/refs.py. Everything else
in the sheet track composes the generic members and is written once.
The ActiveX members were read off the AutoCAD 2026 seat's own type library
(C:\Program Files\Common Files\Autodesk Shared\acax25enu.tlb, "AutoCAD 2025
Type Library" 1.0, the library the running 25.x seat registers) before a line of
the COM path was written, and the measurement is in the code beside the calls:
IAcadModelSpace.AttachExternalReference(PathName, Name, InsertionPoint,
Xscale, Yscale, Zscale, Rotation, bOverlay, Password)
IAcadModelSpace.AddRaster(imageFileName, InsertionPoint, ScaleFactor,
RotationAngle)
IAcadBlock: IsXRef (get), Path (get AND put -- so the repath is an assignment,
not a method), Reload(), Unload(), Bind(bPrefixName), Detach(),
XRefDatabase (get)
IAcadDocument.SaveAs(FullFileName, SaveAsType, vSecurityParams)
AcSaveAsType: ac2000_dwg=12 ac2004_dwg=24 ac2007_dwg=36 ac2010_dwg=48
ac2013_dwg=60 ac2018_dwg=64 (=acNative)
The running seat itself was not answering COM while this landed (its window was
Not Responding and GetActiveObject never returned), so the type library -- which
is the same seat's declared surface and reads without a call into the process --
is what the members are measured from; no document of the operator's was opened
or touched. This repository has been bitten four times by a fake that invented a
member, so the COM fakes in the tests model those names and nothing else.
ActiveX angles are radians, so the degree-to-radian conversion happens at that
boundary and nowhere else.
Headlessly, ezdxf.xref.attach carries the attachment (measured on ezdxf 1.4.4:
block flags 20 for an attachment and 24 for an overlay, so "is this an xref" is
flags & (BLK_XREF | BLK_XREF_OVERLAY) -- masking BLK_XREF alone misses every
overlay), add_image_def + add_image the raster, with Pillow reading the pixel
size so the placed image keeps the file's aspect.
Three capability keys, declared in both maps:
dwg_write -- measured absent here (odafc.is_installed() is False), so the
ezdxf entry is a runtime re-evaluation like viewport_render's
matplotlib check rather than a hardcoded False: installing the
ODA File Converter starts the call working with no edit to the
map. The refusal names the install route and drawing_export_dxf.
xref_live -- reload and bind need a live seat's xref manager and are refused
headlessly; list, detach and path are headless and are not
behind the key.
xlsx_write -- openpyxl is optional and absent here; CSV is always available,
so the refusal never blocks the deliverable and says so.
Two numbers the plan predicted wrongly are corrected to what the registry
actually does. `block` outranks `drawing` in _GROUP_TAG_PRIORITY, so the two
xref tools file under `blocks` (9 -> 11) beside block_insert -- which is where
an xref belongs, being a block definition that lives in another file -- and
`drawing` moves 11 -> 12, not 11 -> 14. And `data_extract` must not say "parts
list" in its own text: that is the golden discovery query for
entity_create_table, and a file writer outranking the tool that draws the thing
is a regression the tuning bar catches. It is described by its destination --
a file -- instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
de8dc8f813 |
fix(task-13): read revision/balloon state from the space it is drawn on, and never at 200
Three findings, one family: a guard that reads the WRONG SET silently stops
guarding and the drawing gets a duplicate or a wrong number with no refusal.
1. Layout-scoped state was read from the wrong space. add_revision called
read_revisions and add_balloon called read_balloons BEFORE enter_layout;
both read entity_list, which follows the current space, so the guard
inspected Model while the rows and balloons lived on the layout -- and the
layout is the sheet workflow (sheet_frame and titleblock_apply both take
one). Measured headlessly: two revision_add(rev="B", layout="SHEET1") calls
both returned created=True, handles 46 and 5A, both bbox
{min [230,273], max [410,287]} -- a second heading band and a second B row
drawn exactly on top of the first, twelve TEXTs on the tab instead of six.
Both reads now happen inside the entered layout, and the whole body sits in
the try/ so the caller's tab comes back on the refusal path too.
2. bom_extract truncated the parts list at 200 INSERTs and printed a wrong QTY
into a real TABLE. extract_records passed its own limit=200 default straight
to entity_list, the tool exposed no limit, and nothing in the payload said
the list had been cut: 250 bolts reported [("ISO 4014 - M12x60", 200)].
all_entities now pages entity_list (offset loop, PAGE_SIZE 1000, plus a
no-new-handles guard so a backend that ignored offset cannot spin) and
extract_records(limit=None) reads the whole drawing. bom_extract exposes
limit (0 = no cap) and reports truncated measured, by asking for one record
past the cap.
3. The balloon clash guard did not fire in two routine cases. (a) targets is
optional in the tool, so the clash test compared () != () -- False -- and
two untargeted balloons carried item 1 with no refusal. The renumber arm
above has already returned for the balloon that stands for these targets, so
anything still carrying the number is a clash, whatever its targets; the
message names the handle and says "no targets" when it has none. (b)
read_balloons and read_revisions took entity_list's 200-entity default, so a
drawing whose first 200 CIRCLEs are a hole pattern read back no balloons at
all and every guard passed blind -- and index = len(existing) restarted at
0, stacking revision bands. Both readers now go through all_entities.
Evidence, headless (ezdxf), each finding's own repro before -> after:
1 created True/True, handles 46+5A, 12 TEXTs -> created True/False, one
handle 46, 6 TEXTs; renumbered False/False -> False/True, same handle 33
2 records 200, rows [(..., 200)] -> records 250, rows [(..., 250)]
3a two balloons handles 2F+35, no refusal -> refused, naming balloon 2F
3b read_balloons() == () with 206 circles -> the balloon past the 200th
circle is found, the rerun renumbers, the item-1/BBB clash is refused
3c 200 notes + rev A twice + rev B -> created True/False/True, row 1 then
row 2 (was: every band at row 1, on top of the last)
Six regression tests added: one per finding plus the layout-scoped twin of
each, which is what would have caught all three.
The live engine was UNAVAILABLE for this round:
win32com.client.GetActiveObject("AutoCAD.Application") did not return within
90 s against the running acad.exe (PID 40068), and the ComBackend probe timed
out on its first call at both COM_CALL_TIMEOUT=60 and 180. No second AutoCAD
was launched and no document was opened. No new ActiveX call is introduced by
this fix -- it only reorders existing calls and pages entity_list(offset=),
which both engines already implement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
01213fc5f5 |
feat(task-6): dimension intents, ISO 286 fits and an overlap-free layout
engineering/mech/dimension.py collects what the features asked to have dimensioned, adds the part's own sizes (chain, baseline or ordinate for the axial lengths, a diameter per segment and per bore, the two overall sizes of a plate), and removes the duplicates - ISO 129-1 says each dimension appears once, and a test asserts no measurement is placed twice. Fits and tolerances are resolved through the modules that already own them: engineering.fits for an ISO 286 code and engineering.tolerances for an explicit ISO 129 tolerance, including build_dim_override's sign convention (the lower deviation is passed as the magnitude of the minus side, exactly as server.py::_fit_to_tolerances does). A H7 bore at 40 mm resolves to +0.025/0 - the correctness check the spec names. A fit together with an explicit tolerance, a fit outside the authored ISO 286 range, and a fit on an intent with no measurable nominal are each refused by name rather than guessed. The layout is the part that had to be measured rather than asserted: every intent's text is turned into a rectangle by the same helper the test uses, and an intent is placed on the lowest level whose rectangle is free, with the level pitch clamped to clear the text whatever the caller passed. The test then asserts the placed rectangles are pairwise disjoint - including on a crowded plate and with a deliberately too-small step. That is the same property the repository's dim_overlap critique focus checks on the finished drawing. The plan's layout clamped that pitch to the text HEIGHT on every axis, which is only the stacking direction for the row of dimensions under the view. Beside the view the levels march in x, so the extent they have to clear is the text WIDTH: measured here, three diameter labels are 9.45 mm wide (9.45, 5.6) at a 8.0 mm pitch, so a shaft's three segment diameters - all on the y = 0 centreline, hence all on different levels - overlapped by 1.45 mm. The pitch and the within-level span are now both taken per axis from the same _text_size helper text_rects uses, which makes the disjointness a property of the algorithm rather than of the label lengths that happened to be in the test. Over hole_table_threshold holes the per-hole diameters leave the view and become a table instead, grouped by diameter, thread, counterbore and depth, one tag per group and the quantity on every row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
978c1075ad |
fix(task-5): cut faces on the real chord, cross sections that see features, a section with no dashed cut edge
Three defects, all of them the same mistake in three places: the cut face was built from something other than the geometry it claims to show. 1. A prismatic cut face removed a feature by its BOUNDING BOX. `Hole.removal_box` is a square, so the gap punched in the cut face was the full diameter wherever the plane passed. On the plan's 80x50x8 plate with a Ho10 hole at (40, 25): a plane at y=29.9 grazes the hole (true chord 1.990) and the section still showed an 8 mm gap through solid metal - 560.0 mm2 hatched against a true 624.08, a 64 mm2 error in the hatched area AND a wrong drawn outline. `Slot` had the same obround-as-box. Features now answer `cut_spans(part, p1, unit)` with their own exact chord: `_circle_chord` for a hole, the union of two circles and a rotated rectangle for a slot, the polygon's own crossings for a pocket, and an exact slab clip of `removal_box` as the default. `_box_span` (the corner projection, which is the box's shadow and not a chord at all) is gone. 2. A transverse revolved section ignored every feature and the honesty rule never fired. A cross section through a DIN 6885-1 keyway came out as a plain hatched disc with `omitted == ()` - the commonest mechanical cross section there is, hatched straight over the 12x5 keyway. The face is now built by `_transverse_face`: its radii come from `radial_profile` (so a groove, an undercut, a bore or an ISO 6410 internal thread is in the face already, which the raw segment diameters could not report), a non-axisymmetric feature is asked for its own `cross_section` notch record, and one the plane really passes through with no such record is listed in `omitted` with its reason rather than hatched over. `Keyway._notch` is the one calculation the end view and the cut face both come out of. 3. A section drew its own cut boundary as a HIDDEN (dashed) line, because the longitudinal branch reused `silhouette_prims` and the silhouette draws the bore dashed - correct for an un-cut view, wrong for a section, where that line IS the hatch loop's boundary. ISO 128-50 bounds a surface lying in the cutting plane with a continuous wide line and ISO 128-3 leaves hidden detail out of a sectional view; via ROLE_LAYER['hidden'] this would also have put the cut boundary on the HIDDEN layer in Task 7. New `section_profile_prims` emits the cut outline all-continuous, and `half=True` keeps the dashed bore on the -Y half only, which is the half a half section does not cut. And the claim the last commit made without earning it: the hatched area and the drawn outline are now literally one calculation. The longitudinal loop is the *emitted* outline flattened (`_flatten_path`), so a chamfer or fillet that substitutes a silhouette corner is cut out of the hatch exactly where it is drawn - a 2 mm 45 deg chamfer takes its 2 mm2 off the 1000 mm2 half face instead of being hatched over. Eleven new tests, every one of them red on the parent commit: the hole chord at 0/4/4.9 mm off centre, a tangent plane leaving the face whole, the 45 deg slot, the keyway cross section against its own analytic area, the feature a cross section cannot project, the feature the plane misses, the groove read off the profile, the continuous cut boundary, the half section's dashed half, and the chamfered cut face. No new ActiveX member is called: all three fixes are pure geometry in the backend-free `engineering/mech` package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
72bd17d024 |
feat(task-5): sections, cut faces, material hatch and detail views
engineering/mech/views.py grows the two remaining view kinds. A section's cut
faces come out of the same radial profile the silhouette does, so the hatched
area and the drawn outline are one calculation and cannot disagree: a solid
shaft's half-section loop measures exactly the sum of its segment rectangles,
and a bore closes the loop on itself rather than being hatched over.
ISO 6410's section convention falls out of the model rather than being special-
cased: an internal thread's radial_edit already raises the bore to the major
diameter, so the hatch stops there. ISO 128-3's rib rule is a flag on the
feature - a pocket marked no_section_hatch is drawn and left unhatched, while
the rest of the cut face is hatched with the part's ISO 128-50 material.
Four styles, each either implemented or refused by name with the alternative
rather than approximated: full and half on a longitudinal plane through a
revolved axis; full and revolved on a transverse plane (the annulus, flattened
at 2 degrees - 0.015% of the radius, stated in CIRCLE_SEGMENTS rather than
hidden); full and offset on a prismatic part, where through features split the
cut face and pockets reduce it to the remaining thickness. A plane that misses
the part, or that grazes an outline vertex so its crossings cannot be paired, is
refused instead of producing a guessed cut face.
Details scale a circular region of a parent view, emit the ISO 128-34 circle and
letter for the parent and the "A (5:1)" label for the detail itself, and report
every feature that falls outside the circle in omitted - the honesty rule again.
Three test assertions were corrected to the measured truth rather than the
engine bent to them, each verified by running the code:
* the ISO 6410 test asked for the whole cut-face loop's minimum to be the M20
major (r=10). It is not, and must not be: over the 5 mm that is drilled but
not threaded the loop closes on the real 16 mm drill (r=8). The loop is
((0,10),(0,25),(25,25),(30,25),(30,8),(30,8),(25,8),(25,10),(0,10)). The test
now pins both spans, which is the convention it meant to assert.
* the transverse-annulus areas were pinned at rel=2e-4. An inscribed regular
180-gon is exactly 1 - (n/2pi) sin(2pi/n) = 2.0307e-4 under the circle, so
2e-4 rejects the very flattening CIRCLE_SEGMENTS declares. The tolerance is
now 2.1e-4 and the test computes the deficit and pins it, so the number is
derived rather than guessed. CIRCLE_SEGMENTS' docstring now states both
figures: 0.015% radial (the 2 degree chord's sagitta) and 0.0203% of area.
* tests/test_mech_views.py pinned VIEW_KINDS == ("front","side","top"), which
this task deliberately grows. It now pins the three orthographic kinds as the
first three and the full five-kind tuple.
_is_longitudinal takes only the plane; the plan's signature also took the part
and never read it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
499b642fbd |
feat(sheet): revisions, the ISO 7573 parts list and ISO 6433 balloons
SECTION 24 grows to six tools: revision_add, bom_extract, bom_table,
balloon_add.
bom_extract reads. It walks the INSERTs through the generic members already in
the contract -- entity_list, block_get_attributes, entity_get_xdata -- and
returns parts-list rows from the ACADMCP_MECH std_part payload, falling back to
the block's own ATTRIBs and reporting which of the two each row came from. A
block with neither is skipped rather than guessed at, and the tool never writes
to the drawing.
bom_table draws the rows as a real TABLE, 180 mm wide (ISO 7573: the item list
is as wide as the title block), growing upwards from directly above it with the
heading row against the block and item 1 the row above -- which is what ISO 7573
asks for on a drawing. A TABLE is drawn downwards from its insertion point, so
the upward list is inserted a table height above its anchor with the heading as
the last row; one table_layout feeds both the real TABLE and the pure
table_prims, so the two renderings cannot disagree.
balloon_add draws the ISO 6433 reference -- numeral, leader, dot on the item --
and writes the balloon payload naming its targets. A rerun over the same
targets renumbers that balloon instead of drawing a second one, and an item
number already used for different targets is refused by name with the other
balloon's handle.
revision_add appends a row to the revision block, is idempotent per revision
code (a repeat draws nothing at all), and optionally clouds the changed regions
and tags them with the triangle. Clouds are refused up front, carrying
capability "revcloud", on an engine that has no revcloud member -- the COM
backend -- rather than leaving half a revision behind.
Declared: ISO 7200 does not standardise the revision block's geometry and
ISO 7573 does not fix the item list's column widths or row height, so both
reuse this repository's 180 mm / 7 mm ruling and say so in their SOURCE
docstrings. ISO 6433's "numeral at least twice the dimension character height"
is met by the numeral being 1.4x the balloon radius: the 4 mm default clears
2.5 mm dimension text, and balloon_add's docstring tells a sheet dimensioned at
3.5 mm to pass radius=5.0.
Three corrections to what the plan wrote, each measured rather than assumed:
* extract_records read `block_get_attributes(...)["attributes"]`. Both engines
return the flat {tag: value} mapping itself (ezdxf_backend.py:6687 builds it
from `ent.attribs`, com_backend.py:5703 from `ref.GetAttributes()` as
{TagString: TextString}), so every attribute fallback would have been
silently empty -- measured headlessly, where a PLATE insert carrying only a
DESIGNATION ATTRIB extracted to nothing.
* draw_bom_table now reports `entity_type` / `representation` /
`child_handles`. `entity_create_table` returns EntityInfo(type="TABLE") whose
handle is the first CHILD of a composite on the headless engine, so reading
that handle back gives a rule LINE, not an ACAD_TABLE -- the contract
tests/test_table_leader.py already pins. A caller that means to edit the
table afterwards has to be told which of the two it got.
* add_revision reports one `tags` entry per tag asked for ({at, handles}), not
one per entity: a tag is two primitives, so a flat handle list claimed twice
as many tags as the caller requested.
Discovery: `bom_extract` / `bom_table` / `balloon_add` each entered the index
heavy with "parts"/"list" tokens and took top-1 for the bare query "parts list"
away from `entity_create_table` (a BM25 tie broken alphabetically). Narrowed on
my side only -- the reader speaks of the bill of materials, the drawer names a
table, the balloon names an item -- never by touching the golden case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
b2f858212f |
fix(task-4): the end-view occlusion rule, the double-drawn groove, the left view
Three defects in the orthographic view engine, all one missing idea: the engine
had no visibility rule and no single owner for the silhouette.
1. `_end_circles` inverted the hidden-line rule. Walking the bands from the
viewing end it drew a circle only when its radius EXCEEDED everything nearer,
and roled every such circle `hidden` unless it was the first band - so the one
edge that CANNOT be occluded (a step face larger than the material in front of
it) shipped dashed on HIDDEN, and the circle that really is covered was not
drawn at all. `_front_max` is now the rule: an edge of radius r at station x is
continuous when r is larger than every outer radius between x and the observer,
dashed when it is not, and it is always drawn. Bores follow the mirror image -
you see down a bore only while it stays at least as wide. Each distinct radius
is emitted once, because two coincident circles are a duplicate entity.
The same rule now roles a feature's axis-centred end-view circle, whose axial
station comes from `removal_box`; a feature that reports none keeps its own
verdict rather than having one guessed.
Ground truth by ray cast along the axis of the same solid of revolution
(segments 20x25, 40x30, 15x20; observer at +X, probe just inside each edge):
r=10 at x=75 -> first surface x=75 -> VISIBLE (was visible, kept)
r=15 at x=60 -> first surface x=60 -> VISIBLE (was HIDDEN, fixed)
r=12.5 at x=20 -> first surface x=60 -> HIDDEN (was not drawn, fixed)
2. Every axisymmetric groove was drawn twice - once as the notch
`radial_profile` cuts into the silhouette, once by the feature's own
`prims()`, which `_feature_layer` appended unconditionally. Six exactly
coincident LINEs per grooved shaft, which is precisely what
`critique.py::_check_duplicate_entities` matches, so premium rule 7 could
never be met on a part carrying a retaining or O-ring groove. `_profile_owned`
now drops a feature primitive that the body already emitted, and drops a
revolved front view's `visible` silhouette line from any feature that edits
the radial profile: the spec's rule is that grooves cut a notch in the
silhouette, and the notch belongs to the one calculation. Suppression is
"the profile already drew this", not "this feature edits the profile", so a
blind bore keeps the bottom line the profile cannot express, and an internal
thread keeps its ISO 6410 limit line.
A groove straddling a step was worse than duplicated: the feature reads one
surface radius for a span that has two, so it drew a groove floor through
solid material and left the +Y path with four odd-degree nodes. It now has
two - its two ends on the axis.
3. `build_view` validated `side` and put it in ctx, but the prismatic branch
ignored it: `side="left"` returned the right-hand view, with every feature on
the wrong side of the sheet and a symmetric outline rectangle to hide it. An
observer at -X sees screen-right = -Y, so the feature layer is now mirrored
about the outline's mid-Y. The top view is always seen from above and does
not move with `side`; that is now said in the docstring and pinned by a test.
Tests: the two that pinned the inverted rule are rewritten around the ray cast,
and one of them contradicted its own name. Seven tests added, and the prismatic
side-view test's hole moved off the y=25 mirror axis, where it could not tell a
left view from a right one. Nine of them fail against the previous `views.py`
and pass against this one.
Live AutoCAD was unavailable for a SOLPROF cross-check: the running AutoCAD 2026
(pid 40068) reports Responding=False and `GetActiveObject("AutoCAD.Application")`
never returned inside 100 s, so no scratch document was created and none is left
open. No new ActiveX member is called by this change - views.py is pure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
34079ad0b4 |
fix(task-11): hatch the cut face, not its chord, and take the scaffolding with it
draw_material_hatch's handles branch read only the traced loop's `points`
and dropped the parallel `bulges`, so a cut face bounded by an arc was
hatched as the chord polygon - exactly the failure README.md and the
comment at backends/ezdxf_backend.py:1172-1176 name. MEASURED headless on
a 40x10 box closed by a 180 degree r=20 arc: boundary_from_entities ->
area 1028.318531, draw_material_hatch -> {'ok': True, 'points': 4} and
entity_measure(hatch) -> {'area': 400.0, 'exact': True}. 628.3 mm2, 61% of
the face, silently unhatched, and the payload stated no accuracy.
entity_create_hatch takes a vertex list on both engines, so an arc edge has
to reach it as vertices; the bug was dropping the curve, not the fact of
vertices. Each bulged segment is now expanded onto its real arc by
_flatten_bulge, spaced so no chord departs from it by more than the
declared HATCH_FLATTEN_SAGITTA (0.01). Same repro after: points 53, area
1027.905195 against the exact 1028.318531 - an error of 0.413 (0.040%),
and the payload says so rather than claiming exactness: `accuracy`
"flatten_tolerance" or "exact", `flatten_tolerance`, `curved_edges`,
`area` (the polygon actually written) and `boundary_area` (the loop's own
exact area) so the two can be compared instead of trusted. `accuracy` is
decided by comparing those two numbers, so a backend that reports no
`bulges` at all is still reported approximate rather than assumed exact. A
straight-sided face reads `exact` / 400.0 as before.
The handles branch also had no area guard - the "at least three points"
refusal only ever covered `boundary` - so a LINE plus a 180 degree ARC
traced to a two-vertex loop and wrote a HATCH of area 0.0 with ok: True.
That case now hatches the real semicircle (37 points, 156.880337 of
157.079633), and a loop that genuinely encloses nothing (three collinear
segments, boundary_from_entities area 0.0) is refused by name before
anything is written: "the hatch boundary encloses no area (measured 0)".
Second: boundary_from_entities does not merely compute a loop, it *writes*
one. The traced LWPOLYLINE was left in the drawing on layer 0 and named
nowhere in the result, so the caller could neither find nor delete it, and
drawing_critique / drawing_finalize scored it against the drawing under the
layer-discipline rule. MEASURED headless on a 40x10 rectangle of four
LINEs: 4 entities before, 6 after, by_type {'LINE': 4, 'LWPOLYLINE': 1,
'HATCH': 1}, the polyline on layer '0'. After: 5 entities, {'LINE': 4,
'HATCH': 1}, and the result carries traced {'handle': '34', 'removed':
True} - the boolean is reported rather than assumed, so a delete that
failed is visible. The delete runs in a `finally:`, so the refusal paths
above do not leave scaffolding either (3 entities before and after).
Third: the module SOURCE block asserted the cutting-plane line's thin
middle was "the `center` role (CENTER, 0.25 mm)" and that the pair gave
"the ISO appearance" at 2:1. engineering/layers.py:19 defines CENTER at
0.18. MEASURED headless after draw_section_line on a fresh EzdxfBackend:
layer_list reports GEOMETRY lineweight 50.0 and CENTER 18.0, i.e. 0.50/0.18
= 2.78:1. A fabricated number in a standards SOURCE block is the one thing
these blocks exist to prevent, so the block now states the measured pair,
names the ratio as 2.78:1, and does *not* quote the standard's own
wide:narrow ratio, which this module cannot transcribe. The wrong figure in
the previous commit message (
|
||
|
|
bf8b3baf74 |
fix(task-9): put the fastener plan in projection with its elevation, and refuse a thread size that is not d
Two wrong-drawing bugs in SECTION 22.
1. _hex_points defaulted to start_deg=0, putting vertices at 3 and 9 o'clock,
so the plan of an M12 hex head measured 18 mm (across flats) across the
vertical while the elevation drew that same head 20.78 mm tall (across
corners e, silhouette edges at +/- e/4 - the projection of a hexagon with
vertices top and bottom). Place the two blocks in projection and the two
views of one part disagree. The default is now 30 deg, named HEX_START_DEG
and documented on both the constant and _hex_points as the convention the
elevation is the projection of, so a future fourth caller cannot pick the
other one by accident. The three top builders (_hex_fastener_top,
_hex_nut_top, _socket_head_top) inherit it.
2. _thread_feature read the pitch from params['size'] via the ISO 261 table
while drawing the geometry from params['d'], never cross-checking the two.
feature_spec('thread', {'d': 8.0, 'length': 20.0, 'size': 'M20'}) drew an M8
minor diameter and returned dims['pitch'] = 2.5 with ISO 261 cited as the
source - one thread's pitch on another thread's geometry, a silent wrong
number carrying a standards citation. A mismatch is now refused by name,
like every other check in the module. std_feature_draw's params description
says so.
Guard tests: the old top-view test asserted only len(points) == 6 and never a
vertex coordinate, so the orientation was untested. Added the vertex
coordinates, the plan-vs-elevation silhouette-height invariant across ISO 4014
/ 4017 / 4032, the socket screw's dk invariant, and the refused thread pair.
No ActiveX member is added or changed by either fix; both are pure geometry and
validation, so no live measurement is cited. The live AutoCAD could not be
probed for a confirmation run - GetActiveObject blocked past 150 s under
sibling contention.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
48eb057273 |
feat(task-4): the orthographic view engine - silhouette, hidden lines, layout
engineering/mech/views.py projects a part model onto a view plane and returns primitive descriptions, so a test asserts geometry rather than a screenshot. The one calculation everything comes out of for a revolved part is the radial profile: the part cut into bands of constant bore and constant (or linearly tapering) outside, with every feature's radial_edit applied. The front view is that profile walked as a path and mirrored about the axis; the section cut face of the next task is the same profile walked as a closed loop, which is what stops the outline and the hatched area ever disagreeing. Chamfers and fillets do not draw over the silhouette - they substitute their corner in it, so the overshoot the critique focus looks for cannot be produced by construction, and a corner edit that finds no corner is reported rather than dropped. A band's radii are read off the segment the band's MIDPOINT lies in and the taper is evaluated at the band's own two ends. Sampling a hair inside the band to dodge the segment-boundary ambiguity - the way the plan's draft did - writes the taper slope times that inset into every coordinate the silhouette is built from: a 30 -> 10 mm cone over 20 mm then reports 14.99999 for a radius that is exactly 15, and nothing downstream can tell that it is wrong. The end view applies a real hidden-line rule: walking the bands from the viewing end, a diameter larger than everything nearer than it is drawn hidden, a bore behind material is hidden, and the near end's own circles are visible. Looking from the left reverses it. A revolved part's top view is refused, because it repeats the front. Prismatic parts get the outline in true shape with corner fillets and chamfers substituted in, and side/top thickness rectangles - the case where a hand-drawing model reliably forgets the hidden line per through feature, which here comes out of the feature itself. The honesty rule of spec section 6 is enforced in one place: a feature that emits no primitives and edits neither the radial profile nor a corner is listed in the view's omitted tuple with its own reason. Nothing is silently dropped. layout_origin places views in first-angle positions by default (the view from the right goes to the left, the view from above goes below) and flips both for third angle. It takes one additive keyword beyond the pinned signature, child, because placing a view to the left depends on that view's own width; without it the child is assumed the parent's size and every pinned call still works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
ffa8327770 |
fix(task-12): keep the plan's task-12 body in step with the shipped fix
The plan embeds Task 12's module and tests verbatim, so it still carried the `_current_space` read and the derivation that put the third-angle cone's short side away from the circles. Both replaced with what shipped, so a re-run of the task cannot transcribe the defects back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
e57ae8b3ac |
fix(task-12): restore the caller's tab from the layout listing; source the projection symbol
enter_layout read the caller's space from `backend._current_space`, which only
EzdxfBackend has. On ComBackend the getattr fell through to "Model", so
`sheet_frame` / `titleblock_apply` with a `layout=` dropped a live caller onto
the Model tab instead of putting them back -- the opposite of what the
docstring and both tool docs promise, and a dual-engine parity break in new
shared infrastructure. Measured on the live seat (AutoCAD 2026, scratch
document from drawing_new(), closed with discard):
scratch document: Drawing24.dwg
layouts: ['Layout1', 'Layout2', 'Model']
B) current BEFORE: Layout2
B) the value the OLD code read (backend._current_space):
<<absent -> getattr default 'Model'>>
`layout_list()` already reported the right value two lines earlier on both
engines (ezdxf from $TILEMODE / the active layout, COM from
`doc.ActiveLayout.Name` -- read live as "Layout2" above), so `previous` now
comes from `listing["current"]`.
The projection-angle symbol was justified by an in-code derivation that cannot
decide the question, and the test restated the implementation. The derivation
was also applied inconsistently: the third-angle symbol mirrored the cone as
well as the end view, leaving the trapezoid's short side pointing AWAY from the
circles -- which is the first-angle arrangement. Transcribed instead from two
independent reproductions of the ISO 5456-2 figure, both downloaded and
measured (ISO's own figure is paywalled and was NOT read; the module says so):
* FreeCAD's ISO 5457 sheet template A3_Landscape_ISO5457_advanced.svg,
`first_angle_trapezoid` = "m 389,222 -10,2.5 v 5 l 10,2.5 z" -> 5-unit side
at x=379, 10-unit side at x=389; circles r2.5/r5 at cx=396.
* Wikimedia Commons "Convention placement vues dessin technique.svg", both
symbols side by side, labelled FR and US: FR 35.856 @ x=13.343 / 66.135 @
x=81.869, circles x=148.005; US 35.856 @ x=380.275 / 66.135 @ x=448.801,
circles x=314.140.
Both draw the trapezoid the same way -- short side left -- and move only the
end view: first angle puts it right of the cone, third angle left. Equivalently
the short side points away from the circles in first angle and towards them in
third, which is how every source states it in words. `third` now follows.
Tests: the reference figures are transcribed into the test as data, and the
symbol is asserted to land its three landmarks in the order each figure does,
so the test cannot pass by agreeing with the code. The layout restore is pinned
through a proxy that hides `_current_space` from the real backend rather than a
fake that owns members ComBackend does not have. Both new tests fail on the
previous code (measured: "assert 16.0 < 8.0" for the mirrored cone; "- SheetB
+ Model" for the tab).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
457c53644f |
fix(task-3): keyway end-view arc, the machined face, and the chamfer's segment bound
Three defects in the feature catalogue, all faithfully transcribed from the plan text and all wrong in shipped code. 1. Keyway.prims(view="side") emitted the arc the keyway REMOVES. `Arc` is CCW start -> end, and the emitted (right wall -> left wall) is the ~31 degree cap over the slot opening, not the ~329 degrees of shaft that survives it. Measured live on AutoCAD 2026 (Documents.Add scratch doc, closed in a finally): ModelSpace.AddArc(center, radius, start_rad, end_rad) -- angles in RADIANS, CCW start -> end -- gives ArcLength 8.098 mm for the emitted 74.534 -> 105.466 deg and 86.150 mm for the reversed 105.466 -> 74.534 deg, against a 94.248 mm full circle at r = 15; StartPoint/EndPoint of the emitted arc are exactly the notch walls at x = +/-4. Swapped, so the arc now runs left wall -> right wall the long way round, and the side view's bbox goes from (-4, 11, 4, 15) to (-15, -15, 15, 14.457). 2. Hole and Pocket cut the plate from OPPOSITE faces in the same view frame: Hole ran z = 0 -> depth, Pocket z = thickness -> thickness - depth, and neither takes a face parameter. A plate carrying both showed the pocket milled from one side and the blind hole drilled from the other, silently -- omission() returns None for both, so nothing reported it. The frame comment now names the machined face (v = thickness, the face the front view looks at), and Hole drills from it: the blind floor, the counterbore step and the through bore all hang down from there, as Pocket already did. 3. Chamfer.validate bounded the axial leg against the whole part, never against the segment the chamfer sits on. A shallow lead-in passed validation and was drawn straight through a shoulder at a radius that segment does not have (size 30 at 10 deg on a 15 mm end segment: radial 5.29 < 10 so the radial guard was happy, yet `before` landed at x = 45, two segments away). New helper _anchor_segment names the segment a corner actually cuts into -- at a step, the adjacent one carrying the larger radius -- and the leg is bounded by its length, refused by name with the step it would have crossed. Tests: the keyway test asserted only primitive counts and two x coordinates, so it passed with the arc reversed; it now pins the sweep, the endpoints and the bbox. The hole test exercised only a through hole, which is symmetric; added a blind-hole-and-pocket test that pins both to the same face, and a counterbore test. Added three chamfer tests (two refusals, one that must still be accepted). Each fix was mutation-checked: reverting it fails its test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
84c1ef42fb |
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 |
||
|
|
64a57d74a0 |
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
|
||
|
|
c607e75b75 |
fix(task-10): ensure annotation layers live, and put the weld symbols and their dimensions where ISO 2553 puts them
Three blocking review findings in engineering/mech/annotate.py.
1. draw_annotation_prims assigned ROLE_LAYER[role] without creating the
layer. EzdxfBackend._apply_attrs adds a missing layer; ActiveX does not.
MEASURED live on AutoCAD 2026 (probe: GetActiveObject, Documents.Add(),
doc.Close(False) in finally): on a scratch document whose layer table is
['0'], draw_surface_texture(ComBackend(), at=(0,0), ra=3.2,
machining='required', lay='perpendicular', allowance=0.5) raised
"AutoCAD COM error (-0x7ffdfff7)" and left one orphan AcDbLine on layer 0
— a partial write with no rollback, on exactly the foreign-drawing path
the spec names (drawing_open runs no bootstrap). The same probe after the
fix: {'ok': True, count 6, 'layers_created': ['DIM','TEXT']}, model space
['AcDbLine' x4 on DIM, 'AcDbText' x2 on TEXT], layer table ['0','DIM',
'TEXT']. New helper ensure_annotation_layers() creates every target layer
up front, before the first entity, with its ENGINEERING_LAYERS colour /
linetype / lineweight; the result reports layers_created.
2. side='both' drew the two elementary symbols 0.4h apart — the other-side
one still stood on the (correctly omitted) identification line, so a
double-V butt weld came out as two detached halves that were not even
symmetric about the reference line. ISO 2553 places them back-to-back on
the continuous reference line: the other-side baseline is now 0.0 when
there is no identification line. tests/test_mech_annotate.py asserted the
wrong apex (-0.4*h), so the gate certified the defect; that test is
rewritten to assert the mirror pair, plus a double-fillet case.
3. For side='other' the size and length were written above the reference
line while the symbol hung below the identification line — 'a6' sat where
an arrow-side throat dimension sits, on a joint with no arrow-side weld.
Both texts now sit beside their own symbol (0.25h clear of the line it
stands on, on the symbol's side). New test covers side='other' with size
and length, and pins arrow/both unchanged.
All five new or rewritten tests fail against the pre-fix module (the layer
one with the measured 'Key not found' refusal, via a fake that models only
that measured behaviour) and pass after. tests/test_mech_annotate.py: 29
passed. Full suite: 3326 passed, 6 pre-existing failures unrelated to this
change (verified identical on the branch tip with the change stashed),
test_release_consistency.py::test_document_numbers_match_the_tree
deselected as instructed. No tool registration changed, so
docs/tool-inventory.json regenerates byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
ac6c4075ba |
feat(task-3): the feature catalogue and its standards tables
engineering/mech/features.py is the nineteen typed features of spec section 5,
each answering the same four questions - is my placement legal, what do I
contribute to this view, to a cut face, and what dimension do I want - plus the
five structural hooks the view engine needs: removal_box, radial_edit,
corner_edit, outline_edit and omission. Pure arithmetic; a test asserts emitted
primitive tuples, not a screenshot.
Nothing restates a table the repository already owns: Keyway takes its width
and depth from engineering.keyway (DIN 6885-1) and is refused by name outside
that table's bore range; GearTeeth takes its outline from engineering.gear's
involute generator and is refused when its tip diameter disagrees with the
segment it sits on.
standards/threads.py transcribes ISO 261 Table 1 and Table 2 - first-choice
M1.6-M64 plus second-choice M14/M18/M22/M27/M33/M39/M45/M52/M60 - and computes
ISO 724's basic minor and pitch diameters from ISO 68-1's own formula, pinned
against the published values (M20x2.5 -> 17.294 / 18.376). The remaining
second-choice diameters, every third-choice diameter and fine pitches below M8
are not transcribed and are refused by name, never interpolated. The drawing
rules are ISO 6410: minor at 0.8 x major, a thread-length limit line across the
major, a three-quarter circle in the end view, and an internal thread whose
section hatch runs to the major diameter.
standards/{undercuts,rings,centres,orings}.py ship DIN 509, DIN 471/472,
DIN 332-1 and ISO 3601-2 with their SOURCE, their row-key contract and an
**empty** row set. That is deliberate and is the track's table rule taken at its
word: a row that could not be verified against the standard is not shipped,
because a wrong relief depth or groove diameter is a wrong workshop drawing
while a narrow table is merely narrow. Every lookup is refused by name and says
what to transcribe and where; the features accept explicit dimensions so the
geometry is fully exercised, and DIN 332 form R is refused outright rather than
approximated by the straight 60 degree cone of forms A and B. Filling any of
these tables later is a data edit with no code change, and a shape test already
fails any row that lands without a column the drawing code reads.
Three deviations from the plan text, each measured rather than assumed
(the merge agent should keep exactly one copy of the first):
1. standards/__init__.py (Task 1, on main) refused a registration with no rows
("registered no rows"), which would have made the four not-transcribed
tables impossible to register at all - and registering them is what keeps a
later transcription a data edit. register() now refuses a rows argument that
is not a dict and accepts an empty one; two tests in tests/test_mech_standards.py
pin both halves. No existing test asserted the old refusal.
2. ORingGroove's housing kind is carried by `housing`, not `kind`. A dataclass
field named `kind` overwrites the discriminator _Base.to_dict() writes:
measured, Probe(id='og').to_dict() == {'kind': 'radial', 'id': 'og'}, so
feature_from_dict would then refuse its own payload as an unknown feature
kind and no part carrying an O-ring groove could round-trip through XDATA.
A new test pins the round trip.
3. The plan's end-chamfer test asserted after == (75.0, 10.0) - the corner
itself - and before == (72.0, 8.268), a point on neither surface. The real
edit is before (72.0, 10.0) on the cylinder and after (75.0, 8.268) on the
end face, consistent with the 45-degree start chamfer the same file pins.
The test was corrected to the measured geometry; the behaviour is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
6024723d6b |
fix(task-8): pin every transcribed standards row, not a sampled subset
The module docstring claimed "every value asserted here is a named row of a named table", but the converse did not hold: 27 of the 83 shipped rows had a dimension no test compared against a literal. ISO 4014/4017 M5 (s,k), ISO 4032 M5 (m), ISO 7089 M5 (d1,d2,h), ISO 4762 M4/M6/M10/M16 (dk,s,b) and D/B for 20 of the 33 ISO 15 bearings were constrained only by inequality tests (series growth) and by the bore-number rule - never by the transcribed number. For a task whose worst failure is "a standards table row that is not in the standard", those rows were exactly where such a row would hide. Measured before the fix with a pytest plugin that rewrote five rows in memory (M5 hex bolt s=k=99, M5 nut, M5 washer, M16 socket screw dk=s=b=99, 6307 as 78x20 instead of 80x21): 32 passed in 0.07s. All five now fail. Each of the five tables is pinned by an ISO*_ROWS dict literal carrying every dimension of every shipped size, parametrized one case per size/designation, plus a completeness test asserting the table's key set equals the literal's - so a row added later without a pin is red too (verified: a fabricated M14 head and 6011 bearing fail the completeness gate). 32 -> 93 tests, 0.06 -> 0.13 s. No production code changed; no ActiveX member is involved, so no live probe was required for this finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
56e6ff7206 |
feat(sheet): ISO 5457 frames and ISO 7200 title blocks for A4-A0
SECTION 24 opens with sheet_frame and titleblock_apply. engineering/sheet/frames.py transcribes ISO 216's trimmed sizes, ISO 5457's 20 mm filing margin and 10 mm edges, the grid reference system (an even number of divisions per side, that side of the frame over 50 mm; letters from the top, numbers from the left), the centring marks (trimmed edge to 5 mm inside the frame) and the 10 x 5 mm corner trimming marks. Every one of those numbers is pinned by a test against the clause it comes from, because a wrong margin is visible on every drawing this server makes. engineering/sheet/titleblock.py draws the ISO 7200 block -- 180 mm wide, which is exactly the drawing-space width of a portrait A4, which is why one block fits every size -- and carries the ISO 128-30 projection-angle symbol. Which symbol is which is derived rather than remembered: first-angle projection puts the view seen from the left on the right, and the view of the cone from its small end is the small circle inside the large one, so first angle draws the cone left and the circles right. The old A3 module folds in. apply_iso_a3_titleblock now forwards to apply_titleblock(size="A3", projection=None, zones=False, marks=False), which is the geometry it always drew, and tests/test_engineering_titleblock.py passes unchanged -- that is the regression gate for the fold. No new ActiveX member is called: both tools reach the drawing only through contract members the A3 title block has used on both engines since v1.0 (entity_create_polyline / _line / _circle / _text, layout_list, layout_set_current), so there was nothing to probe live. Declared, not glossed: the zone divisions drawn here are equal, so ISO 5457's allowance for shorter corner fields is not implemented; the division counts, which are what a zone reference quotes, are the standard's. ISO 7200's full optional-field list is not transcribed, only the fields this block has a cell for. Coverage is A4-A0; an unknown size is refused by name with the five it has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
a4ce46ba9b |
feat(mech): SECTION 23 opens with surface_texture and weld_symbol
ISO 1302:2002's seven transcribed symbol-proportion rows (text height -> line width d', short-leg height H1, minimum height H2) drive the 60 deg vee, the machining-required bar at H1, the machining-prohibited circle inscribed in the vee, and the all-around circle at the kink; the Ra/Rz values, the process, the lay symbol and the machining allowance land around the extension line, which grows with the widest of them. A text height outside the seven rows is refused by name rather than interpolated, and ISO 1302 is accepted as an alias designation for drawings still issued under it - the graphical symbol is the same one. ISO 2553:2019's reference line, dashed identification line (omitted for a symmetrical weld, as the standard permits), size written before the symbol, length and pitch written after it, field-weld flag, all-around circle and process tail. Six elementary symbols are transcribed - square, single-V, single-bevel, single-U, single-J, fillet. The rest of the elementary table is deliberately not shipped: a weld kind outside the six is refused with the list of the six, because a guessed weld symbol is a wrong instruction to a welder. Both tools place the symbol on a plain leader - a line plus an arrowhead with the proportions the backend's own mleader already draws - rather than through leader_create_mleader, which refuses empty text and would hang an MTEXT label off every symbol. Proportions neither standard dimensions (the all-around circles, the flag, the identification line's dashes, the text pitch and the character width estimate) are named constants documented as this implementation's declared choices, never as transcribed values. No new ActiveX member is called: both tools compose LINE / ARC / CIRCLE / LWPOLYLINE / TEXT through the entity-creation contracts both engines already implement, so no live COM probe was needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
49b972f007 |
feat(task-8): ISO fastener and ISO 15 bearing tables, transcribed and pinned
engineering/mech/standards/fasteners.py carries ISO 4014:2011 / ISO 4017:2014 head dimensions (s, k), ISO 4032:2012 nut heights, ISO 7089:2000 washer d1/d2/h and ISO 4762:2004 socket head cap screws, plus the ISO 261:1998 coarse pitch column. bearings.py carries the ISO 15:2017 deep groove boundary dimensions for 6000-6010, 6200-6210 and 6300-6310. Only the ISO 261 first-choice sizes ship. M14/M18/M22/M27/M33, everything above M36 (above M24 for ISO 4762), the 618x/619x/16xx bearing series, bore numbers above 10 and every suffixed bearing variant are refused by name rather than interpolated: a narrow table is merely narrow, a wrong row ships a wrong workshop drawing. Across-corners e is derived geometry (2s/sqrt(3)), so no e column had to be transcribed at all, and the ISO 8826-1 cross and ISO 8826-2 element-symbol fractions are declared drawing conventions, not table values. No ActiveX member is called by this task, so no live AutoCAD probe was needed; the two modules are pure tables and pure arithmetic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
04d269318a |
feat(task-2): the mechanical part model - RevolvedPart, PrismaticPart, build_part
engineering/mech/part.py is the model the whole track draws from: a revolved stack of segments along +X, or a closed prismatic outline plus a thickness, each carrying a list of typed features. Pure arithmetic - no backend, no async, no DXF - which is what lets the view engine be tested without a drawing. Every refusal fires before anything is drawn and names the offending path the way pid_from_spec does: a non-finite or non-positive number as segments[2].d_outer, a bore that is not smaller than its outside (a bore that changes diameter is two segments), an outline with fewer than three distinct vertices, an outline that crosses itself (engineering.measure.is_self_intersecting - the shoelace cancels crossed lobes rather than failing, so a bowtie would otherwise measure 0.0), an unknown material, an unknown segment key, a duplicate feature id. Two gates are the reason this is a model rather than a dict: a feature whose placement is off the part is refused by its own name and by the range it missed, and two features that would remove the same material are refused with both names and the overlapping box. Neither is clamped. part_to_dict round-trips through build_part, which is what lets mech_part_draw write the model onto the drawing as ACADMCP_MECH XDATA and mech_view_add read it back months later without the caller re-describing the part. Two fixes to the plan's own code and test list, both recorded in deviations: _features_from imported engineering.mech.features unconditionally, so a featureless part - every fixture in this module, and both round-trips - died on ModuleNotFoundError until Task 3 lands; the import now happens only when there is a feature dict to build. And the plan's test file omitted test_a_duplicate_feature_id_is_refused while its own gate deselects it by name, so the duplicate-id refusal the commit message claims went unpinned; the test is written here and Task 3 turns it green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
2bb8044144 |
feat(mech): shared foundation - primitives, XDATA codec, standards registry, ISO 128-50 materials
engineering/mech/ opens with the four modules every track-B/G branch imports, so the four worktrees can be cut from this commit and none of them has to invent a name the others will conflict with. - primitives.py: the typed descriptions the pure modules emit (Line, Arc, Circle, Poly, HatchArea, Text, DimIntent) with bbox / translate / rotate / scale, and ROLE_LAYER - the one place an ISO 128 line role becomes a layer name, so no module hardcodes a layer. The arc bbox sweeps its quadrants: a 0-180 arc reports y=+r, not its endpoints, which is what keeps the view layout from stacking two views on top of each other. - xdata.py: the ACADMCP_MECH codec in Track A's shape - a 1001 application marker, 1000 chunks of at most 255 ASCII characters, the 16 KB per-entity ceiling enforced before anything is written, and a foreign application, a corrupt chunk stream, a wrong version or an unknown kind refused with ValueError instead of decoded into something plausible. - standards/__init__.py: the table registry. A size outside the coverage is refused against the coverage; a size inside it that is not a listed row is refused against the rows. Nothing is interpolated, an ambiguous variant is never resolved by taking the first row, and a registration whose SOURCE does not name the table it was transcribed from is refused. - standards/materials.py: the section-hatch map for nine materials. ISO 128-50 standardises only the general hatching (narrow continuous lines at 45 degrees) - that is acadiso.pat ANSI31 and what engineering/gear.py already draws for steel. The distinguishing symbols are ASME Y14.2M's section lining as shipped in AutoCAD 2026's acadiso.pat, read off the installed file; cast iron's departure from ASME and the absence of any mechanical symbol for concrete and wood are stated in the rows themselves. Angles are 0.0 because the patterns already carry 45 degrees, and the two architectural patterns' scales are derived from the ANSI31 line spacing and recomputed by the test rather than chosen. No tool is registered here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
a8482a5f0d |
docs(spec): align tracks B+G with the written plan
The five gaps the plan's reconciliation found, each resolved in the spec: part placement lives in draw_part(at=, rotation=) so the dataclasses carry no origin; engineering/sheet/refs.py is gone with the contract correction; image_attach takes no clip and titleblock_apply no position (the ISO 7200 corner is fixed by the standard) — both recorded as out of 1.6; balloon_add places one balloon per call against the item numbers bom_extract already assigned. Records the four feature tables that ship empty (DIN 509, DIN 471/472, DIN 332-1, ISO 3601-2): no row could be verified against the standard here, so the structure, the row-key contract and the refusal ship without rows, and the features draw from explicit dimensions until the tables are transcribed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
5497dc01d9 |
docs(plan): tracks B+G — 16 TDD tasks in five groups
Assembles the skeleton and the five drafted parts into one plan, Task 1 through Task 16, each a TDD task with its failing test, its implementation and its gate. Groups: - (main) Task 1 — primitives, the ACADMCP_MECH codec, the standards registry and the ISO 128-50 material map; the shared foundation the four branches all consume. - M (Tasks 2-7) — the part model, the feature catalogue with its DIN/ISO tables, the view engine (orthographic, then sections and details), dimensioning, and SECTION 21's six tools. - S (Tasks 8-9) — the ISO 4014/4017/4032/7089/4762 and ISO 15 tables, and SECTION 22's three tools. - A (Tasks 10-11) — ISO 21920-1 surface texture, ISO 2553 welds, centre marks, the ISO 128-40 cutting-plane line and the material hatch: SECTION 23's five tools. - G (Tasks 12-14) — ISO 5457 frames and ISO 7200 title blocks A4-A0, revisions, the ISO 7573 parts list and ISO 6433 balloons, xrefs, images, data extract and DWG: SECTION 24's eleven tools and the four contract members. - F (Tasks 15-16) — the six mech_* critique focuses, the mech pack, lean, aliases and golden queries; then the v5 benchmark, the correctness checks, the live COM smoke and the docs. Interface pass over the assembled file: View.dims is tuple[DimIntent, ...] as the skeleton pins it; every XDATA writer calls Task 1's to_values rather than re-deriving the chunk split from encode; stdparts' five standard features are STD_FEATURE_KINDS so they do not collide with features.FEATURE_KINDS; the revision block carries its own ACADMCP_SHEET_REV application instead of a fourth ACADMCP_MECH kind that the pinned KINDS would refuse; and Task 16 states 39 correctness checks, which is what it registers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
c97be8fc40 |
docs(plan): tracks B+G skeleton — 16 tasks in five groups, interfaces pinned
The header, global constraints, file structure, the interface block every task builds against, the group/ownership table and the merge notes. Task sections are drafted next. Also corrects the spec's architecture section: the engineering tools compose the generic contract members the way gear/keyway/titleblock already do, so only four operations (xref_attach, xref_manage, image_attach, drawing_export_dwg) need contract members and an implementation per engine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
5ff66b641f |
docs: tracks B+G (mechanical 2D and the sheet standard) design spec
One part model — a revolved or prismatic profile plus typed features — and one view engine that projects it into front/side/section/detail views with hidden lines, cut faces and dimensions, instead of a generator per part shape. Around it: the standard-parts catalogue as code inserted as blocks with attributes, the ISO annotation symbols, and the sheet standard (ISO 5457 frames A4-A0, ISO 7200 title blocks, revisions, ISO 7573 parts list, balloons, xrefs, DWG). 25 tools in four sections, new pack `mech`, three capability keys, six mech_* critique focuses, benchmark task `mech_assembly` and six correctness checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
39e6e2f9e4 |
docs(settings): track E shipped — CLAUDE.md sections 18-20, README, changelog with the live COM smoke record
CLAUDE.md gains items 18-20 (styles, page setup & templates, environment), the grown drawing_settings facade on item 11, lean 55 / 52 and TOOL_PACKS=all|core,pid,settings, and three conventions: layer states are ours (ACADMCP_LAYERSTATES XRECORD, mode "xrecord"), page-setup evidence is the PDF's own /MediaBox, nothing interprets coordinates in UCS. README token table re-measured on this tree with benchmarks/token_suite.py (ratio/v1 estimates): full 199 tools / 59,315 tokens, core,settings 190 / 55,593, core 168 / 48,825, lean 55 / 16,360, search 2 / 356; corpus re-derived from discovery.aliases: 186 AutoCAD command names, 933 synonym phrases, 204 tools. Styles & standards row, page setup & templates row, final environment wording, and "Known limitations of the settings track". CHANGELOG Unreleased: the track E intro, Added (styles, page setup & templates, environment, drawing_settings growth, benchmark evidence), Fixed (the Track A hardening list), Changed (three groups, packs, measured idle cost, release snapshot 204 · 8 · 5 · 3284). Spec status: implemented. Where the plan's text and the tree disagreed, the tree wins: mleaderstyle_create is implemented on both engines through the ACAD_MLEADERSTYLE dictionary and no `mleaderstyle` capability key exists (spec §4, capability maps), so the docs say so instead of calling it ezdxf-only; the Task 7 fix (tolerant ACADMCP_PID payloads, crossings tested against the arc) joins the hardening list the plan left it out of. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
18c7c69da6 |
fix(task-25): page_setup_truth and settings_pdf_mediabox_a3 move the sheet to ANSI B before proving A3
A fresh Layout1 is already ISO A3 landscape (ezdxf R2018 default: 420 x 297, plot_rotation 0), so an A3-only /MediaBox read-back passed with page_setup_apply stubbed to a no-op: the A/B gate row and the v4 matrix task could not fail on the setter. Both now apply ANSI B (432 x 279, a size no fresh document has; spec section 9 names it) and read that PDF first, then apply ISO A3, read its PDF, and require the setter's changed.size_mm to have moved from B to A3. The stub fails at the first PDF. The moved-sheet comparison is +/-0.5 mm, the spec's tolerance, not exact: measured live on AutoCAD 2026 (scratch Documents.Add(), closed unsaved), the COM engine reports ANSI B as 431.8 x 279.4 and the DWG To PDF.pc3 file's MediaBox as 431.8 x 279.4 then 420.158 x 297.039 for A3; the task passes there with changed.size_mm [[431.8, 279.4], [420.0, 297.0]]. Headless it is [[432.0, 279.0], [420.0, 297.0]]. tests/test_benchmark_v4.py gains the mutation test the finding describes (setter stubbed -> both rows must fail; both passed on the old code) and a run of the gate row itself; the headless task test asserts the B stage. README rows and the tasks_v4 docstring say what the task now does; the README collected-tests figure follows the two new tests (3282 -> 3284). The regenerated A/B report against v1.5.1 is byte-identical (26 -> 32/32). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
c2942d3c94 |
bench: matrix v4 adds page_setup_truth; correctness suite grows to 32 with three settings checks (A/B vs v1.5.1: 26 -> 32 / 32)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
02dbb133b5 |
feat(settings): live COM smoke on AutoCAD 2026 builds the five .dwt twins and records the run
scripts/smoke_settings_com.py --build-dwt exited 0 on AutoCAD 2026 (25.1s), 2026-09-22: five templates/*.dwt written through drawing_template_save (SaveAs code 66, each file opens with AC1032), page setup plotted and the PDF's own /MediaBox read back as 420.158 x 297.039 mm, iso_a3_mech.dwt reopened as source "bundled" with ISO-25 current, layer state / named view / UCS / preferences / SummaryInfo / prompt line all through the COM engine. The record is in CHANGELOG.md under Unreleased. The smoke keeps a ledger of the documents it opens and closes them on the way out, so a failed run never leaves scratch documents on the operator's seat. Two of its expectations were corrected to the merged tree: mleaderstyle_create is implemented on COM (spec: no `mleaderstyle` key), and plot_style_list marks catalogue rows with `installed`. --interactive was not run unattended on the shared seat (an unanswered GetPoint holds the command line); Task 23 measured the pick live. *.dwt / *.dwg are binary in .gitattributes; AutoCAD's plot.log is ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
b54271f4ac |
fix(com): four live defects the settings smoke measured on AutoCAD 2026 — rejected calls after a document switch, typelib spelling of color/Lineweight, narrowed layer-state dictionary, layer 0 Freeze put
Measured 2026-09-22 by scripts/smoke_settings_com.py --build-dwt against the running AutoCAD 2026 (25.1s), each on a scratch Documents.Add() document: 1. RPC_E_CALL_REJECTED (-2147418111) on Documents.Count straight after Documents.Open, after Close(False) inside document_close, and in _ensure_document_state: AutoCAD's own message filter refuses incoming calls while it is still switching documents. pywin32 exposes no CoRegisterMessageFilter, so _wait_out_rejected_call waits the window out for read-only callables (0.25 s -> 1 s pauses, 20 s budget, inside COM_CALL_TIMEOUT) at those sites and after Documents.Add/Open; any other com_error is not retried. 2. layer_list raised "'IAcadLayer' object has no attribute 'Color'. Did you mean: 'color'?": acax25enu.tlb spells IAcadEntity/IAcadLayer `color` and `Lineweight`. Late-bound dispatch resolved .Color/.LineWeight case-insensitively; with a makepy cache present (this machine's gen_py was regenerated on 2026-09-17, and every object a method returns is then a generated wrapper) the lookup is exact. All 17 sites now use the typelib spelling, which works on both wrappers; the fakes model the measured names. 3. layer_state_restore raised "'IAcadObject' object has no attribute 'Count'": Dictionaries.Item and IAcadDictionary.Item are declared IAcadObject* (the narrowing _com_unnarrow already handles for MLEADER styles). The save path went through the typed Dictionaries.Add, so only the reopen paths failed. Both now un-narrow; the fakes narrow like pywin32. 4. The Freeze put on layer 0 raises 'Invalid layer' whatever the value, even False while thawed (color/Linetype/Lineweight/Plottable/LayerOn/Lock/ ActiveLayer all fine on it), so a state saved with 0 current restored with two warnings and 0 unapplied. Freeze is written only when it changes the layer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
0514f8f15d |
chore(templates): rebuild the five bundled DXFs on the merged tree (styles via dimstyle_create/textstyle_create, iso_a1_arch annotation scale 1:50)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
d072df9fd4 |
fix(task-24a): apply_standard decides each unit write on a read taken after the dimension style switch
|
||
|
|
f7528b2fa5 |
fix(com): every system variable is read and written on the ActiveX document, not the Application
AcadApplication has no GetVariable/SetVariable members; they live on
AcadDocument. Measured on the running AutoCAD 2026 (25.1s, 2026-09-22,
scratch Documents.Add(), Close(False)):
hasattr(app, 'GetVariable') -> False
hasattr(app, 'SetVariable') -> False
app.GetVariable('INSUNITS') -> AttributeError: ... IAcadApplication
instance ... has no attribute 'GetVariable'
doc.GetVariable('INSUNITS') -> 4
doc.GetVariable('MEASUREMENT') -> 1
doc.GetVariable('FILEDIA') -> 1; doc.SetVariable('FILEDIA', 0) -> None,
reads back 0, restored to 1
doc.GetVariable('AUDITCTL') -> 0, 'CMDACTIVE' -> 0, 'USERS1' -> ''
Fifteen call sites in backends/com_backend.py still went through the
Application inside `try/except ... log.debug`, so on a live seat they
silently degraded: MEASUREMENT defaulted every seat to metric,
drawing_info reported units from a failed INSUNITS read, drawing_audit
never set AUDITCTL, the CMDACTIVE guards of system_run_command /
system_run_lisp never fired and run_lisp never read USERS1 back. All are
rerouted through `_acad_doc()`; the try/except stays only where an
optional read is defaulted (INSUNITS -> "Unknown", AUDITCTL absent on a
host that lacks it, USERS1 after the expression ran) and the log line
says so.
Linetype loading, measured the same way: `SendCommand("_-LINETYPE _LOAD
CENTER acadiso.lin\n\n")` loads nothing with FILEDIA=0 and leaves the
command at `Enter an option [?/Create/Load/Set]:` (LASTPROMPT), which
makes the whole seat answer RPC_E_CALL_REJECTED to every client until
ESC is pressed; with FILEDIA=1 it opens the modal "Select Linetype File"
dialog. `doc.Linetypes.Load("CENTER", "acadiso.lin")` returns None and
the table grows 3 -> 4; a repeat raises 'Duplicate record name', an
unknown name 'Undefined linetype', a missing file 'File system error'.
`_ensure_linetype_loaded` and `linetype_load` now call `Linetypes.Load`
(MEASUREMENT still read on the document to pick the .lin), touch no
FILEDIA, and report a failure with the English excepinfo description.
`apply_standard` reads every variable it will write before the first
style is created, so a read the engine refuses is a refusal with nothing
written (it used to fail after ISO-25 / ISOCP were created and current).
Fakes: every application fake refuses GetVariable/SetVariable
(tests/test_styles.py's COM fixture carried them on the Application,
enshrining the wrong shape); the members exist on document fakes only.
Regression tests per rerouted site: linetype load via Linetypes.Load with
MEASUREMENT on the document, drawing_info units from the document,
AUDITCTL set/restored on the document, CMDACTIVE/USERS1 on the document,
apply_standard refusing before any write.
Live end-to-end with the real ComBackend on a scratch document:
apply_standard('iso') -> INSUNITS 4, ISO-25 (already in acadiso.dwt,
created: false) and ISOCP (created: true) current, second call created:
false / changed: {}; linetype_load('CENTER') -> Linetypes 3 -> 4, FILEDIA
stays 1, repeat already_loaded, 'NOSUCHLT' refused by name;
_ensure_linetype_loaded('HIDDEN') -> 3 -> 4; drawing_info units 'mm';
drawing_audit AUDITCTL 0 -> 0; system_run_command ok;
system_run_lisp('(+ 40 2)') -> '42'.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
70afd57831 |
feat(server): settings pack — 22 environment tools behind TOOL_PACKS, lean grows to 55, three groups, golden set +10
Task 24 (merge sanity) on main after the five track E groups merged.
- TOOL_PACK_NAMES = ("core", "pid", "settings"); PACK_TOOL_NAMES["settings"]
is exactly the 22 SECTION 20 tools. Styles and page setup stay in core.
- LEAN_TOOL_NAMES += page_setup_apply, batch_plot (53 -> 55).
- _GROUP_TAG_PRIORITY / _GROUP_TAG_LABELS: "style" moved up next to "pid",
"pagesetup" -> page_setup and "environment" -> environment added; the 28
SECTION 19/20 decorators gain their section tag, so _tool_groups() reports
styles 10 / page_setup 6 / environment 22 (23 groups, 204 tools) and
pack == group == section for the settings pack.
- SECTION headers, backends/base.py composition, contracts/__init__.py
exports and the layer_states feature entry were already correct after the
merge (read and confirmed, nothing edited).
- Pack text: config.py, .env.example, README config row; README <sub> line
204 tools in 23 groups; release snapshot 3259 collected tests (measured).
- docs/tool-inventory.json regenerated; registry snapshot re-derived.
- tests: test_tool_packs.py replaced (11 tests), test_capability_contract.py
+3 (composition, the agreed capability flags, layer_states honesty),
golden set +10 tuning cases (81 = 66 tuning + 15 holdout); all ten route
in the top 3 without alias edits.
Deviation from the plan text: TRACK_E_KEYS carries five keys, not six. The
plan's "mleaderstyle" (ezdxf True / com False) contradicts spec §8.1, which
retracted that key because the ActiveX route exists (§4.2); measured on the
tree: capability_keys() == {boundary_trace, chspace, interactive_prompt,
live_application, preferences, revcloud, wipeout}, and
ComBackend.mleaderstyle_create is a native implementation
(backends/com_backend.py:6752, is_capability_default -> False). The test now
also asserts the key is absent.
No new ActiveX call was written in this task, so no live probe was needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
90c1194f7f |
merge: track-e group V
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
8e05b5e8eb |
merge: track-e group P
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
3278586c6e |
merge: track-e group C
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
0a581a8da3 |
merge: track-e group S
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
6b3477081c |
merge: track-e group H
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
d313da783a |
docs(analysis): Track A review backlog — every minor finding with a disposition
Waves 1-4 of the Track A review produced ~170 minor findings that lived only in the orchestrator's scratchpad. Filed de-duplicated, one line each: fixed-in-task-N (this track's hardening tasks), fixed-in-track-A (with the commit), accepted-limitation (with the reason) or deferred-1.7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
efcc8bbba1 |
fix(task-7): crossings test the exact arc, so a line through a jump's apex counts
flatten_bulges chops a semicircle into an even number of chords (36 for r=50, 12 for r=5), so a flattened vertex lands exactly on the apex — the axis of symmetry, the one place a P&ID line most plausibly meets a crossing jump. segments_cross treats a vertex hit as a junction, so both neighbouring chords dropped it and the arc contributed 0: the (45,0)->(55,0) bulge -1 jump reported crossings 0 at x=50 and 1 at x=50.3 or x=49, the plan's own (0,0)->(100,0) semicircle 0 at x=50, and the mirrored frame (extrusion -Z) 0 at x=-50. count_crossings now takes bulges= (per-chain DXF bulge lists, zero-padded) and tests every bulged edge as its exact arc through the new segment_arc_crossings — circle/segment intersection restricted to the sweep — with the same junction rule as the straight case: a hit within _EPS of the segment's ends, of the arc's own two ends, or a tangent, is not a crossing. Nothing along the sweep is a vertex any more. The arc parametrisation is factored into _arc_of_bulge and shared with flatten_bulges, which keeps its promised signature and its ezdxf pin but no longer feeds the crossing count. draw_line passes the vertices and bulges through unflattened. Pinned in tests: the apex case on the semicircle (x=50, y 20->60), the r=5 jump at x=50 / 50.3 / 49 on +Z and -Z frames, the junction cases (T-touch on the arc, through the arc's endpoint, chord side, tangent along the apex, in-and-out at y=4.9 counts 2), and 4x400 seeded random segment/arc pairs against ezdxf's ConstructionArc.intersect_line (a 20 000-case run in the worktree found 0 mismatches). The eight new assertions fail on the flattened rule and pass on the exact one. Headless finding, headless evidence; no new ActiveX call was written, so the shared live AutoCAD was not touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
d3ef7b45a2 |
feat(discovery): style golden queries; dimstyle/textstyle set_current and drawing_apply_standard join the lean profile
Five tuning cases for the styles surface (DDIM, the ISO-25 set-current paraphrase, the one-call drafting-standard paraphrase, the isocp text style, and the dimstyle counting question) — measured against the live catalog every one ranks top-1, so no alias vocabulary changed. The docstring count moves to seventy-one (56 tuning + 15 holdout); the holdout set is untouched. Lean decision (spec §8.2): dimstyle_set_current, textstyle_set_current and drawing_apply_standard join LEAN_TOOL_NAMES (50 -> 53); the seven authoring/list tools stay full-only and all ten stay in core (no pack claims them). Pinned by the new profile test in tests/test_styles.py, written async like the rest of that module (its module-wide asyncio mark warns on a sync test). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
9cb4bee587 |
fix(pid): existing line payloads are read tolerantly and crossings follow arcs, not chords
A foreign or hand-edited ACADMCP_PID payload (from: 'garbage', seq: True) made the next pid_line_draw raise TypeError; payload_port_refs / payload_seq now accept only well-formed values and a reader result never raises. An existing polyline with a bulge was tested as its chord, so a line through the arc reported crossings 0; flatten_bulges (pure, pinned against ezdxf.math's flattening) feeds count_crossings on both engines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
a5d166f365 |
feat(styles): drawing_apply_standard — ISO or ANSI styles, units and layers in one idempotent call
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
b458923e19 |
feat(templates): drawing_template_save on both engines — dwt_write is a declared capability
Live AutoCAD writes a real .dwt through SaveAs(path, ac2018_Template = 66) and puts the description into the summary Comments; headlessly a .dxf template is written through drawing_save_as and a .dwt request is refused with capability dwt_write, naming the DXF route and the COM route. drawing_save_as's "dwt" entry was 5 (acR13_dxf): a DWT request wrote an R13 DXF under a .dwt name. Probed live on AutoCAD 2026 (typelib: ac2018_Template = 66, acR13_dxf = 5, acTemplateDwg absent; the saved file starts AC1032, the document is rebound, Comments takes). SECTION 19 is now 6 tools; SAVEAS joins SHARED_ACAD_COMMANDS; templates/README.md documents the two files per template. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
cfb5d5082c |
fix(task-6): the reader no longer hands a signal line the neighbouring pipe's label
pid_graph step 5 ran the TEXT label fallback for every edge without a
number. Once signal lines stopped carrying a fabricated number (
|
||
|
|
021a4eb43d |
fix(task-11): dimensions_using_style is one filtered selection set, not a per-entity COM walk
_com_dimensions_using walked every entity of every layout and, since the un-narrowing fix, re-dispatched each one through a dynamic proxy before reading ObjectName (~8-10 ms per entity), which put dimstyle_modify past the 60 s COM_CALL_TIMEOUT on a ~6k-entity drawing after its SetVariable + CopyFrom had already landed. It is now a single SelectionSet.Select(acSelectionSetAll, (0 . "DIMENSION") (3 . <style>)), which answers from AutoCAD's own index whatever the drawing size. Measured live (AutoCAD 2026, scratch doc, 1000 LINEs + 14 dimensions across model and paper space): walk 12.7-15.8 s, selection 0.02-0.09 s, identical handle sets; the whole dimstyle_modify tool now returns in 0.07 s. The group-3 filter value is a wcmatch pattern, so the style name is backtick-escaped (_wcmatch_literal): unescaped, "A.B" also selected the dimensions of "A_B", "N#1" those of "N21", "P[1]" those of "P1", "S@X" those of "SAX" (measured). Handles are returned in ascending handle order, Select failures propagate, and the selection set is deleted either way. tests/test_styles.py: the fake document carries a SelectionSets that emulates ssget "X" over every layout with wcmatch-literal semantics and narrowed IAcadEntity items, refusing an unescaped wildcard; three tests cover no-per-entity-read, escaping, and cleanup-on-failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
17f7228fd6 |
fix(task-19): a .dxf template really reaches the live seat; the fallback dimvars land on Standard
Documents.Add(<file>) accepts only a genuine DWT. Measured on AutoCAD 2026: Add(templates/iso_a3_pid.dxf) returned the default acadiso drawing (layers ['0'], Layout1/Layout2) exactly as Add() and Add(<missing.dwt>) did, so drawing_new(template=<bundled name>) reported ok:true with source 'bundled_dxf' over a blank document, and the explicit .dxf path route was just as blank. ComBackend.drawing_new now converts any non-.dwt template through AutoCAD itself (Documents.Open read-only -> SaveAs ac2018_Template, older constants tried next -> Close(False)), caches the .dwt in the temp folder keyed on path/size/mtime, hands Add the .dwt, and refuses a missing template file instead of letting Add default silently. The tool result carries template.dwt / template.dwt_cached. Re-measured end to end through server.drawing_new on the real ComBackend: ansi_b_mech -> 11 layers, tab B, DIMTAD 0 / DIMTIH 1 / DIMEXO 1.5; iso_a3_pid -> 17 layers, tab A3, second call reuses the cached .dwt (0.8 s conversion once). Fake-ActiveX tests pin the call shape and that the conversion document never stays open. Headlessly the build's fallback wrote spec 4.1 only as header DIM variables, and the renderer folds six of them, so DIMTAD/TIH/TOH/EXE/EXO/GAP/LUNIT/LWD were a no-op: ansi_b_mech dimensioned with ISO text placement while system_get_variable and AutoCAD said ANSI. The fallback now also stores each value on the Standard dimstyle; the five DXFs are rebuilt (--check passes) and a test reads the effective values off a DIMENSION drawn from each template and asserts header and style agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
3c0babfef1 |
fix(pid): signal lines carry no fabricated number; a bubble exits towards the first waypoint
An ISA-5.1 signal line has no pipe line number, yet every pneumatic / electric line got the bare sequence as its number and a label, and ate a process sequence number. Signal classes now leave line_number None (a verbatim one is still honoured), seq None, no label. A radial port aimed at the far end even with waypoints, so the first segment cut through the bubble; aim_points (lines.py) aims at the first/last waypoint for both draw_line and the dry run. Merge note: benchmarks/adapters/autocad_mcp_pro.py (final group's file) had its two _task_pid_roundtrip assertion lines moved to the new truth — the signal row is unnumbered, the connector row is 3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
03a5067d83 |
fix(task-11): live text styles write TrueType by typeface, refuse missing fonts and arrowhead blocks before any write, un-narrow the dimension walker
Four review findings against the COM styles block, all measured on AutoCAD 2026
(scratch documents from Documents.Add, closed without saving):
1. textstyle_create: `TextStyle.fontFile = "arial.ttf"` raises 'Filer error' for
every bare TrueType name (bundled presets included) after TextStyles.Add has
run, leaving a stub with an empty font that a retry meets as "already exists";
a full path is accepted but the STYLE record then carries the machine path.
`SetFont("Arial", bold, italic, 0, 0)` — the STYLE dialog's own route — stores
the clean `arial.ttf`. The font is now located before Add (`_com_locate_font`:
support path, then the Windows font folders), a TrueType is written by the
family/subfamily read from its `name` table (`_truetype_face`), an SHX by the
name given, an unlocatable file is refused with nothing written, and a write
ActiveX still refuses after Add deletes the entry it added.
2. _ensure_com_textstyle goes through the same helper, so DIMTXSTY / text_style
ARIAL and ISOCPEUR are created on the first call with a real font instead of
failing once and then silently binding a fontless style.
3. dimstyle_create / dimstyle_modify check a user DIMBLK/DIMBLK1/DIMBLK2 against
doc.Blocks before any write (`_com_require_arrowhead_blocks`, the headless
`_require_arrowhead_blocks` twin, same message and table-spelling rewrite);
a write that fails after DimStyles.Add restores the previous ActiveDimStyle
(discarding the unsaved overrides) and deletes the half-made style and any
preset text style the call created.
4. _com_dimensions_using un-narrows each `Layouts.Item(i).Block.Item(p)` —
declared IAcadEntity*, so `.StyleName` raised AttributeError under a makepy
cache and the blanket except reported `[]` — and swallows nothing.
The fake in tests/test_styles.py now models the measured ActiveX rules (bare
TrueType fontFile refused, SetFont by installed typeface, SetVariable refusing an
unknown arrowhead, IAcadEntity narrowing on block items, Delete) so the old code
cannot pass; nine tests cover the four findings. SECTION 18 docstrings state the
live engine's refusal rule for fonts and the pre-write arrowhead check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
dda11755cf |
fix(task-16): custom keys follow AutoCAD's simple case compare, and on COM AutoCAD routes the write
|
||
|
|
a76d9e08a3 |
feat(templates): five bundled templates built by the server's own tools; drawing_new(template=name)
SECTION 19 grows to five tools with drawing_template_list; drawing_new takes a
bundled name (iso_a3_mech, iso_a1_arch, iso_a3_pid, ansi_b_mech, ansi_d_arch)
or a .dwt/.dxf path and reports template: {name|path, path, source}.
scripts/build_templates.py builds templates/*.dxf through EzdxfBackend
(layer set, header DIM variables until group S's styles land, drawing_settings,
sheet tab, ISO 5457 frame on A3, page_setup_apply) and --check pins them
reproducible. One drift the plan's same-process measurement could not see is
fixed at the source rather than masked: ezdxf orders CLASSES by set iteration,
so two processes disagreed on LAYOUT / ACDBPLACEHOLDER; the section is re-keyed
(required list first, then by name) before the save.
The root .gitignore ignores *.dxf as generated output; templates/*.dxf is
un-ignored there, since the bundled set is the one DXF that is committed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
86c5d6ad65 |
fix(task-18): the headless Model plot is doc.modelspace(), not the current tab
EzdxfBackend.drawing_export_pdf routed layout=None / layout="Model" through _msp(), which follows the current tab, so with a sheet current the "Model" PDF was that sheet's paper-space content with no refusal — the same defect finding 3 named on COM, still live on the headless engine. Measured: circle in model space, 390x270 rectangle on an A3 sheet, sheet current — the Model row's /MediaBox was 176.1x121.9 mm (the rectangle) instead of 121.9x121.9 (the circle). The model target is now doc.modelspace() whichever tab is current, on both entry points. The test that vouched for this only asserted the row's shape (paper is None, true by construction). It is replaced by two tests that prove content: the Model row's mediabox with a sheet current equals the one with Model current and carries the circle's square aspect, and drawing_export_pdf(path) with no layout does the same. Both fail without the backend change. plot.py's docstring now says which engine does what. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
2e101b95e2 |
fix(task-5): a rollback that fails after a cancellation no longer kills the session, and the COM rollback goes back to an UNDO Mark instead of prompting
Two review findings on
|
||
|
|
a60f801205 |
feat(server): SECTION 18 styles — dimstyle/textstyle/mleaderstyle tools with DIMSTYLE/STYLE/MLEADERSTYLE aliases
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
4fe2d963c0 |
fix(task-16): custom keys mirror AutoCAD's full AddCustomInfo rule, deletes are exempt, keys match case-insensitively
The |
||
|
|
f6951e2597 |
fix(task-18): the COM plot is foreground, by layout name, and confirmed on disk; plot_style_list reads PrinterStyleSheetPath
Three review findings against SECTION 19 on the live engine, each measured on AutoCAD 2026 in a scratch document with the operator's BACKGROUNDPLOT=2: - plot_style_list read Preferences.Files.PrintStyleSheetPath; the ActiveX member is PrinterStyleSheetPath, so the AttributeError was swallowed and every row came back installed=None. Renamed in the backend, the server docstring and the test fake (which faked the same wrong name). Live: 13 rows, all 9 catalogue ctb files installed, four .stb files as installed. - ComBackend.drawing_export_pdf returned ok before any file existed under background plotting, so batch_plot died on path.stat() with an unhandled FileNotFoundError and a later foreground plot raised E_FAIL. The export now sets BACKGROUNDPLOT=0 around PlotToFile (restored after), honours the method's bool and reports ok only with the file on disk (bytes included). batch_plot turns a backend RuntimeError or a phantom ok into a per-row ok:false, keeping the rows already plotted. - With a sheet current, the Model row was that sheet plotted again: the COM export switched tabs only when a layout was named. layout=None now makes Model current (restoring the tab), and batch_plot names every layout, Model included, so both engines plot the tab the row is labelled with. Live: Model row /Title (Model) at 210x297 beside the A3 sheet, the active tab and BACKGROUNDPLOT restored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
2052915f34 |
fix(task-23): user_select reports the picked point in WCS — GetEntity answers in the current UCS, so translate acUCS→acWorld before returning
Measured on AutoCAD 2026: under a UCS at (100,50) rotated 90 deg, a pick on a circle at WCS (105,70) came back from IAcadUtility.GetEntity as (20,-5,0), while Utility.GetPoint already answered in WCS. A model doing ucs_set -> user_select -> entity_create_* at `picked` would place geometry at a silently wrong position. The point goes through Utility.TranslateCoordinates(acUCS, acWorld) packed as a VT_ARRAY|VT_R8 VARIANT (the plain tuple GetEntity hands back is refused as "Invalid argument Point"). The fake Utility now models the UCS frame and the VARIANT requirement; a new test pins the measured figures and checks GetPoint stays untranslated. Docstrings state WCS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
266b6fb838 |
fix(task-10): un-narrow the ACAD_MLEADERSTYLE dictionary route so mleaderstyle_list/create work on the live engine
acax25enu.tlb declares IAcadDictionaries.Item, IAcadDictionary.Item and IAcadDictionary.AddObject as returning IAcadObject*. With a makepy cache for the AutoCAD type library present (it is, on any machine where gencache ever ran against AutoCAD), pywin32 wraps each result in the gen_py class of the declared type, which exposes only IAcadObject members: .Count/.Item/.GetName on the dictionary and .ArrowSize/.LandingGap/.TextHeight/.TextStyle on a style all raised AttributeError, so the previous round's removal of the @capability refusal turned a typed UnsupportedCapabilityError into a raw AttributeError on AutoCAD 2026. `_com_unnarrow` re-dispatches an object on its raw IDispatch through win32com.client.dynamic.Dispatch (no gencache side effect, unlike CastTo), and the dictionary route applies it to the dictionary, to every Item(i) and to the AddObject result. Verified live on AutoCAD 2026 (scratch document, closed afterwards): list reads Standard = 0.18/0.09/0.18/Standard, create writes ISO-LDR = 2.5/1.0/2.5/ISOCP, the duplicate is refused, and an independent late-bound read of the dictionary agrees. The fake in tests/test_styles.py now models the narrowing: Dictionaries.Item, Dictionary.Item and Dictionary.AddObject hand back a wrapper that answers only IAcadObject members and raises on everything else (reads and writes alike) until unwrapped through _oleobj_. Five COM tests fail against the previous backend and pass now; a new test pins the missing-dictionary path (Dictionaries.Add before AddObject). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
2b9791fbcd |
fix(task-16): custom properties refuse pre-R2004 headlessly, line breaks, and AutoCAD's invalid keys before any write
Three header-only-write / half-write classes in drawing_properties_set:
1. Measured: ezdxf emits $CUSTOMPROPERTYTAG / $CUSTOMPROPERTY only after
$LASTSAVEDBY, which does not exist before R2004, so on an R12 (AC1009) or
R2000 (AC1015) document the write reported ok, drawing_properties_get
echoed it from memory, and drawing_save_as (version kept) wrote nothing --
the class
|
||
|
|
f25421709f |
feat(pagesetup): SECTION 19 — page_setup_list/apply, plot_style_list, batch_plot with /MediaBox read-back
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
b83a74e969 |
feat(environment): system_launch, preference whitelist and operator prompts on COM; declared refusals headlessly
user_pick_point / user_select answer {cancelled: true} on ESC (DISP_E_EXCEPTION)
and {timed_out: true} past COM_CALL_TIMEOUT; preferences_set refuses read-only,
unknown and out-of-range keys before any ActiveX call.
The read-only key is Files.PrinterStyleSheetPath, not the spec's
PrintStyleSheetPath: IAcadPreferencesFiles has no member of that name
(measured live on AutoCAD 2026 / R25.1), and a misspelt key would make
preferences_get() raise on every live call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
32821c6410 |
fix(task-5): pid_from_spec shields its rollback so a real client cancellation rolls the sheet back on both engines
The MCP SDK cancels a request by cancelling an anyio scope, and anyio cancellation is level-triggered: task.cancel() is re-issued on every loop iteration while the task is inside the cancelled scope. run_spec's `except BaseException: await backend.transaction_rollback()` was therefore cancelled at its first await, before the rollback body ran. Measured through the real transport (fastmcp.Client + notifications/cancelled): five symbols left in an open transaction on ezdxf; on the COM shape EndUndoMark / _UNDO B never sent while the backend's `finally` already cleared _transaction_active, so the next transaction_begin nested a StartUndoMark. A native Task.cancel() (what the plan's Step-4 note reasoned about) is delivered once and clears, which is why raising CancelledError from a mock passed trivially. The rollback now runs inside anyio.CancelScope(shield=True). The COM hard timeout (asyncio.wait_for inside ComBackend._run) still fires inside the shield, measured at 0.62 s with a 0.5 s deadline, so a hung rollback cannot freeze the server. Tests: the mock-raise test is replaced by real cancellations that fail on the unshielded code — a cancelled anyio task-group scope (parametrized alongside the native Task.cancel() case), the real transport via Client.cancel(request id) polled mid-run, and the COM shape (real ComBackend._run, a real single-thread executor and the real transaction_rollback over a recording ActiveX document, with a call in flight when the scope is cancelled) which asserts StartUndoMark, EndUndoMark, _UNDO B land and the next begin is fresh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
10a7372ce7 |
feat(settings): drawing_properties_get/set — SummaryInfo on COM, custom properties on both engines
Summary fields are refused headlessly with capability dwgprops before any write; custom properties are $CUSTOMPROPERTYTAG/$CUSTOMPROPERTY pairs and survive save + reopen. Validation is shared, by name, and never str()-coerces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
54bbf72312 |
fix(task-17): an inch sheet's fit reports paper units per drawing unit, not the mm ratio times 25.4
`_paper_frame` computed `paper_per_unit = mm_per_unit * unit` where `unit` is millimetres per paper unit, so an extents+fit plot on an inch sheet reported `effective_scale: "607.06:1"` for a fit that is `1:1.06276` (off by 25.4 squared). The render was right (`units_per_mm` is unit-free); only the label was wrong, and it flowed into `drawing_export_pdf(layout=...)`. The mm sheet could not see it because there `unit` is 1. `test_extents_plot_fits_the_printable_area_and_centres` is parametrized over ISO A3 (mm, width-bound) and ANSI B (inches, height-bound); the ink position is derived from the centred frame so both cases check the same invariant. The inch case fails without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
e204f227b4 |
fix(task-10): $DIMBLK header carries the canonical arrowhead name; MLeaderStyle is authored on COM through the ACAD_MLEADERSTYLE dictionary
Two review findings, both root causes.
1. `_sync_header_to_dimstyle` copied the DIMSTYLE entry's ezdxf-internal
arrowhead spelling (OBLIQUE) into the DXF-facing $DIMBLK/$DIMBLK1/$DIMBLK2
header, so the saved file named a block it did not hold (the blocks are
_OBLIQUE / _DOT) and `system_get_variable("DIMBLK")` answered "OBLIQUE"
headlessly against "_OBLIQUE" on COM and in the same engine's own
`dimstyle_list` row. The header write now goes through
`reported_arrowhead` — the same read-side rule `_dimstyle_value` applies.
Pinned by header, `system_get_variable`, save/reopen (`ezdxf.readfile`
header equals the block the file holds) and the Restore-to-Standard
empty-string case in `test_builtin_arrowheads_render_save_and_report_one_spelling`.
2. The `mleaderstyle` capability refusal rested on a false engine boundary.
acax25enu.tlb (AutoCAD 2026) exposes IAcadMLeaderStyle with Name,
ArrowSize, LandingGap, TextHeight, TextStyle, ... — exactly what
`Dictionaries.Item("ACAD_MLEADERSTYLE").Item(i)` returns — and
IAcadDictionary.AddObject(keyword, "AcDbMLeaderStyle") creates one.
COM `mleaderstyle_list` now reads the four values off those objects
(`values_available: True`); COM `mleaderstyle_create` validates every value
(`validate_mleaderstyle`, shared with ezdxf), checks the clash and the text
style before AddObject, then writes the four properties. The contract
method is `@abstractmethod` again, the key is gone from both capability
maps, the fake ActiveX surface models the real call shape, and the spec
(§4.2, §8.1, §9) states the ActiveX route.
Live AutoCAD was not running on this machine (GetActiveObject: operation
unavailable; no acad.exe), so the COM route is type-library-verified and
fake-tested, not executed live — scripts/smoke_settings_com.py (Task 24)
owns that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
c7dd72410a |
fix(task-22): live named views read and restore the display (VIEWCTR/VIEWSIZE, ZOOM Window), UCS sysvars come from the document, and a frozen current layer is thawed before restore makes it current
Four review findings, all measured by the reviewer on AutoCAD 2026:
1. ucs_list and the ucs_restore('world') CMDACTIVE guard read
app.GetVariable, but GetVariable is an AcadDocument member — the tool
was dead live and the guard's swallowed read forced cmd_active to 0,
sending `_.UCS _W` into any open prompt. Both read the document now,
and a failed CMDACTIVE read refuses instead of proceeding. The fake
app in tests/test_views_ucs.py has no GetVariable any more, so the
old code cannot pass.
2. view_named_save defaulted from doc.ActiveViewport.Center/Height/Width,
which does not track the display (it answered the initial view after
`_.ZOOM _C 5,6 20`). The current view is VIEWCTR / VIEWSIZE at the
SCREENSIZE aspect — what `-VIEW _S` records (width 40.267 for height
20). The fake viewport is deliberately stale so a read of it fails.
3. view_named_restore wrote Center/Height/Width into the AcadViewport;
AutoCAD anchors the lower-left corner and widens to the display
aspect, landing a 10x20 view at (5, 6) on VIEWCTR (20.134, 6). It now
zooms the saved rectangle (app.ZoomWindow), which centres and fits
like `-VIEW _R` (VIEWCTR = saved centre, VIEWSIZE = max(h, w/aspect)),
reports `applied: "zoom_window"` and reads VIEWCTR / VIEWSIZE back.
Headless the same reconciliation moved the centre whenever the
*Active VPORT's aspect was rewritten from w/h (even the 1.34 default
opened at x = 11.734): the aspect the file carries is left alone —
it is the display's, written by AutoCAD at save — and the window is
fitted into it; the payload adds `vport_height` / `aspect_ratio`.
4. layer_state_restore set ActiveLayer before the loop and outside the
try; AutoCAD refuses to make a frozen layer current, so the spec's
own save → freeze → restore path raised an opaque COM error with
nothing applied. When `frozen` is being restored the state's current
layer is thawed first; the write is wrapped and a refusal lands in
`warnings`. The fake document and layer now model both refusals.
Live AutoCAD was not running in this round (GetActiveObject failed, no
acad process), so the fix is measured against the reviewer's numbers,
not re-measured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
ac1fba80b7 |
fix(pid): pid_from_spec refuses non-finite numbers by path and rolls back on any interruption
_num let NaN/inf (and a 400-digit int, as OverflowError) through, so an INSERT was placed at [nan, 0] and the refusal, when it came, named the line instead of equipment[0].x. run_spec caught Exception only, so a CancelledError mid-run left the placed symbols inside an open transaction (an open undo mark on COM). Finite check in _num; except BaseException. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
a51c1b6e14 |
fix(task-2): a moved multi-line attribute keeps one placement and its whole value on the headless engine
ezdxf keeps a multi-line ATTRIB's two placement surfaces (the ATTRIB's own insert/align_point and the embedded MTEXT's insert) in step through transform() but not translate(): Insert.translate moves only the ATTRIB's points (1.4.4 attrib.py has no translate). entity_move / entity_copy / entity_array_rectangular therefore left the embedded MTEXT behind, so block_explode burst the note 50 units from its symbol (audit clean), and block_set_attributes / text_find_replace -- which rebuild the MTEXT and let set_mtext re-place the ATTRIB from it -- teleported the attribute back to the old spot while the INSERT stayed put (a regression of e7dafb8/b753a12, which before wrote only dxf.text). - _anchored_mtext(attrib): the virtual MTEXT re-anchored to the ATTRIB's own placement (get_placement() through its OCS, the inverse of ezdxf's _update_location_from_mtext). _burst_attrib and _write_attrib_value read through it, so a drawing whose surfaces already disagree bursts and writes at the ATTRIB's placement instead of the stale MTEXT's. - _translate(ent, ...): translate + settle every multi-line ATTRIB's MTEXT onto the ATTRIB (the set_mtext round trip ezdxf's own transform uses), keeping the whole value in group 1. Used by move, copy, rectangular array. - _transform(ent, m): transform + restore group 1 of every multi-line ATTRIB -- BaseAttrib.transform goes through set_mtext, which mirrors only the first line, so rotate/scale/mirror/polar array/change_space turned 'A\PB' into 'A' on the one surface an R2010 save keeps. - block_insert of a multi-line ATTDEF: add_auto_attribs embeds through embed_mtext with the same first-line mirror; the whole value goes into group 1 the way _write_attrib_value keeps it. Tests: move/copy/rectangular array carry the MTEXT (explode lands on the symbol), set_attributes and find_replace after a move leave the attribute where the move put it, a rotated reference moves both surfaces by the same delta, a stale drawing bursts/writes at the ATTRIB placement, rotate/scale/ mirror/polar and a fresh insert keep the whole value across an R2010 round trip, and a single-line reference is unchanged. 13 fail before the fix, all pass after; full suite 2511 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
bbc9aeeb63 |
fix(task-21): registry identity is the file (st_dev/st_ino), not its spelling — a hard link, junction or case variant reloads instead of registering twice
`_file_identity` keyed on normcase(abspath), which folds case and separators only, and only on Windows. Two names for one file — a hard link (which Path.resolve() cannot fold either), an NTFS junction reached in-process, or a case variant on macOS's case-insensitive APFS where normcase is the identity — registered two entries: both rows said `saved: True`, a save from either overwrote the other's bytes, and closing the 'clean' one dropped its work as `discarded_changes: False`. Identity now asks the disk: `os.stat` (st_dev, st_ino) when the path exists (the samefile identity), `normcase(abspath)` only for a path not on disk yet (a save target, or an owner whose file was removed) or a filesystem that reports no inode. `_register_document`, `_path_holder`, `_resolve_document_key`, the `_doc_path` setter and the quarantine's same-path warning all go through the one helper. An untitled entry holds no file and never matches a path. Tests: the `_case_folding_fs` guard probes the temp filesystem instead of `os.path.normcase`, so the case-variant tests run on macOS where they used to skip while the bug was live. New: hard link (backend and over the wire), junction (mklink /J; a directory symlink elsewhere), a case variant with normcase given its POSIX definition, and the spelling fallback for a save target that is not on disk. Four of the five fail on the previous backend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
b464bcdaf5 |
fix(task-4): block_define dedupes requested layers case-insensitively so one layer in two spellings is created and reported once
DXF and ActiveX layer tables are case-insensitive, but block_define collected the primitives' layers with a case-sensitive set. A request naming 'Alpha' and 'ALPHA' with create_layers=True made ezdxf add the first, then raise DXFTableEntryError on the second after the write — layer left behind, no block, surfaced as an internal error instead of a named refusal. COM ran Layers.Add twice, got the existing layer back the second time, and reported two layers created for one that appeared; the two engines diverged and the refusal message without create_layers named the same layer twice. referenced_layers() in backends/block_specs.py keys the set on the folded name and keeps the first spelling; both engines consume it. _FakeLayers in the hardening tests now matches Item/Add case-insensitively and returns the existing record on a duplicate Add, as the ActiveX table does, so the COM path is pinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
375c9f4c70 |
fix(blocks): block_define refuses a non-finite base point and a missing layer unless create_layers
base_x/base_y were only float()-cast, so a NaN base point was written into the BLOCK record; a primitive on a layer the drawing lacks was written as a dangling reference doc.audit() never flags (and on COM would fail mid-loop after Blocks.Add). validate_base_point names the key; the layer table is probed before the first write and the missing names are refused, or created and reported as layers_created when create_layers=true (new optional kwarg, contract + both engines + tool). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
7f79497928 |
fix(blocks): attribute values are validated before any write; null/bool/objects refused by tag
block_insert and block_set_attributes applied str() to whatever arrived, so a JSON null became the text None and a non-mapping failed after the INSERT was written. validate_attribute_values (backends/block_specs.py) runs first on both engines: strings pass the one-line rule, ints and finite floats are written as plain digits, everything else is a TypeError naming the tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
31daac022f |
fix(task-21): layout_create, layout_set_current and viewport_create mark the headless document dirty — the close refusal and the saved row now see them, and each gets an undo step
document_close's promise (never drop unsaved work unless told to) and the `saved` row of document_list are only as good as _DocState.dirty, and three headless mutators never set it: a new layout tab, the $TILEMODE / active layout write, and a paper-space VIEWPORT all changed the file yet closed without refusal, reported discarded_changes: False, and were absent on disk. COM refused the same sequence (Saved is AutoCAD's DBMOD), so the two engines disagreed. Each now calls _mark_dirty() after its write, which also pushes the undo snapshot every other mutator already has. The regression test runs the save -> mutate -> list/close/undo/redo -> close with save sequence for all three and checks the change reached the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
b753a12bb6 |
fix(task-2): find_replace rewrites every surface of a multi-line attribute; COM explode propagates a failing read and undoes a failed burst
Headless text_find_replace matched and wrote only dxf.text of an ATTRIB/ATTDEF, so on a multi-line attribute it reported replaced: 1 while block_get_attributes and block_explode still carried the old embedded MTEXT (and a needle on the second line was never found). The ATTRIB/ATTDEF branch now reads through _attrib_value and writes through _write_attrib_value, the same pair block_get_attributes / block_set_attributes use. COM block_explode swallowed a failing GetAttributes() (attrs = ()), GetConstantAttributes() and Invisible, then exploded, deleted every ATTDEF placeholder and the reference and returned ok: True with no attribute text -- every value destroyed silently on a transient RPC_E_CALL_REJECTED. Those reads now happen before Explode() and propagate with nothing written; a failure after the explode deletes the members it added and the TEXTs written so far and leaves the reference in place (_undo_explode), so the drawing is never left half-burst under an exception. The fakes gain the members a live AcadBlockReference / AcadAttributeDefinition always expose (GetConstantAttributes, Constant, Invisible) instead of relying on the swallow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
b1c93cdd2a |
fix(task-21): the applied ISO layer set is per document — it follows document_activate on ezdxf and is cleared on every document switch on both engines
`drawing_apply_iso_layers` recorded its standard in `_active_layer_set` on
the backend, and `_role_layer` reads it ahead of the PlanSpec to decide where
`dimension_auto` / `construction_xline` file their output. It was neither a
`_DocState` field nor cleared by `_reset_document_state`, so an iso13567
sheet next to a mech one pushed the mech drawing's dimensions onto
`M-DIMEN-T-N` (created on demand), silently — and `drawing_critique` could
not see it because the same stale set drove the critique. The attribute
predates the registry (v1.5 `drawing_new` did not reset it either), but the
per-document claim of Task 21 covered it and the measured attribute list
missed it.
- `_DocState.active_layer_set` + `_ActiveField("active_layer_set")` on
`EzdxfBackend`: the set follows the active entry, bidirectionally.
- `PremiumContract._reset_document_state` clears it (declared on the mixin
like `_plan_spec`), which fixes COM's `_ensure_document_state` switch and
`drawing_new` / `drawing_open` / `disconnect` on both engines.
- tests: `test_active_layer_set_is_per_document` (headless registry, the
reviewer's repro plus the reverse direction) and the COM activate test now
also asserts the set is cleared and `_role_layer('dim')` is back on DIM.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
aeb27c252b |
fix(task-17): the headless page setup writes the scale AutoCAD plots by, and reads the code 75 AutoCAD writes
Two measured root causes on the headless engine, both silent: 1. page_setup_apply wrote PLOTSETTINGS group 147 (unit_factor) as 1.0 for every scale. Under USE_STANDARD_SCALE AutoCAD's plot engine scales by group 147 alone (a 1:10 sheet with code 21 and 147 = 1.0 plotted a 100-unit line at 99.99 mm while the row, `applied` and 142/143 said 1:10; patching 147 to 0.1 alone gave 9.99 mm). AutoCAD writes 147 as numerator / denominator (measured live on AutoCAD 2026 for fit, 1:1, 1:2, 1:10, 1:50, 2:1, 10:1: 1.0, 1.0, 0.5, 0.1, 0.02, 2.0, 10.0). The writer now stores 75, 142/143 and 147 from the one resolved ratio on every route, so no reader can disagree with the plot. 2. DXF_STANDARD_SCALE_TYPE and _plot_scale followed ezdxf's STD_SCALES, which has no 19 = 1:5 and numbers everything above 18 one lower than the ObjectARX StdScaleType enum AutoCAD actually writes (1:10 = 21, 1:20 = 23, 1:50 = 26, 1:100 = 27, 2:1 = 28, 10:1 = 31 — measured via COM SaveAs DXF). An AutoCAD-authored 1:50 sheet listed as "1:100" and a 1:10 sheet rendered at 1:16. papers.py now authors the full file enum (DXF_STD_SCALES, codes 1-33 with AutoCAD's imperial names), the writer uses the same codes ActiveX uses, and nothing goes through ezdxf's table. 1:5 stays on the custom route on both engines (the live setter refuses 19) while the reader knows code 19 for files AutoCAD wrote. Tests: the plan's `1:50 -> 25` assertion was the stale numbering and now pins 26; a pure-data test pins the enum against the measured codes and the ActiveX numbering; a parametrised writer test checks 75/142/143/147 for every catalogue scale; a reader test stores the scale exactly as AutoCAD saves it (including codes 19 and 22 the writer never emits) and checks page_setup_list and _paper_frame report and render it at its own scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
5f33613460 |
fix(task-2): COM burst MTEXT anchors after the attachment; headless multi-line attribute values persist below R2018
Two review findings on the multi-line-attribute paths of block_explode / block_set_attributes. COM: `_add_mtext_from_capture` wrote `InsertionPoint` before `AttachmentPoint`. ActiveX's `AttachmentPoint` keeps the text body where it is and relocates the anchor, so every non-TopLeft attribute was laid out TopLeft-at-anchor with only the label moved (live: a BottomLeft two-line note landed two lines low, BottomRight also a box width right). The anchor is now written last, unconditionally, after the attachment and the frame. Verified live on AutoCAD 2026: every alignment's bbox matches the control and the end-to-end burst MTEXT sits at the ATTRIB's TextAlignmentPoint. The fake MTEXT now relocates `InsertionPoint` on `AttachmentPoint`/`Normal` writes the way ActiveX does, both tests assert where the anchor ended up, and the alignment test uses BottomRight rather than the default TopLeft that could never show the displacement. ezdxf: `_write_attrib_value` relied on `set_mtext`, which mirrors only the first line into `dxf.text`; below R2018 ezdxf exports no embedded MTEXT, so the default R2010 document saved `NEW ONE` where the reader had promised `NEW ONE\PNEW TWO\PNEW THREE`. `dxf.text` now carries the whole value (AutoCAD's own `TextString` convention) after the MTEXT rebuild; the R2010 round-trip test uses a `\P` value, and an R2018 round trip pins both surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
75c13196b9 |
fix(task-15): angle sysvars are radians on both engines; OSMODE is registry-saved and refused headlessly
Three authored claims in SYSVAR_CATALOG were false against the engines: - HPANG said "degrees" but ActiveX GetVariable/SetVariable (and AutoLISP getvar) hold radians - measured on AutoCAD 2026 in a scratch document: SetVariable(pi/4) reads back 0.7853981633974483 on both paths. The row now says radians, like POLARANG. - ANGBASE said "radians" with both engines True, but the DXF header stores $ANGBASE in degrees (AutoCAD saved the 1.5708-radian seat value as 90.0) and ezdxf read/wrote it raw, so the two engines disagreed by a factor of 57.3. EzdxfBackend now translates that header variable at its boundary (_HEADER_DEGREE_SYSVARS): the tool speaks radians on both engines, the file keeps degrees. - OSMODE claimed engines.ezdxf True, but $OSMODE is an R12-only header variable ezdxf never exports for R2000+: a headless write reported ok: True and read back None after save + reopen. It is registry-saved in AutoCAD, so it joins _UNSAVED_SYSVARS (refused with capability: registry_sysvar; the facade's `osmode` key refuses and snapshots None headlessly, like polar / polar_angle). Headless reads of every registry-saved variable now report None instead of whatever the ezdxf template holds in memory. The gate that should have caught all three, tests/test_sysvar_catalog.py's default round-trip, wrote 56 no-ops out of 66 and never saved: it now writes a value that differs from the fresh document's for every writable ezdxf-True entry, saves, reopens and reads back from the file. Two new tests pin OSMODE's refusal and ANGBASE's radians-at-the-tool / degrees-in-the-file split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
64cec37cc7 |
fix(task-22): ucs_list decides "world" from the frame, not the empty name — an unnamed UCS is its own current row
An unnamed UCS (UCS Origin / 3P without saving, the common kind) has an empty UCSNAME exactly like WCS does, so both engines answered `current: world` while the drawing was not in WCS — verified live on AutoCAD 2026 (`_.UCS _O 10,10,0` → UCSNAME="" WORLDUCS=0) and on the DXF it writes ($UCSNAME='' $UCSORG=(0,20,0)). A silent wrong value. - COM: a named match still wins; otherwise WORLDUCS (AutoCAD's own answer) decides, and WORLDUCS=0 appends a `name: None` row built from UCSORG/UCSXDIR/UCSYDIR. - ezdxf: the same rule against $UCSORG/$UCSXDIR/$UCSYDIR via the new `engineering.environment.ucs.is_world_axes`; a stale $UCSNAME falls back to the frame instead of leaving no row current. - Server `ucs_list` result: `current` is null for an unnamed UCS (never `world`), plus `current_unnamed: bool` so null is unambiguous. - Tests: helper, headless (unnamed row, stale name, and the exact AutoCAD DXF over the wire), and COM fake reading WORLDUCS + the frame variables. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
e7dafb85ab |
fix(task-2): headless block_set_attributes rewrites the embedded MTEXT; block_explode refuses members ezdxf would silently drop
Two review findings on the headless engine, both content lost with ok: True. Multi-line attribute writer: `block_set_attributes` set only `attrib.dxf.text`, so after an edit `block_get_attributes` reported the new value while `block_explode` (`_burst_attrib`) burst the embedded MTEXT at its pre-edit content -- and on the default R2010 document, where ezdxf exports no embedded MTEXT, a save/reload carried the new value the exploded drawing never showed. `_write_attrib_value` now rebuilds the embedded MTEXT from its own virtual entity with the new content (`virtual_mtext_entity()` + `set_mtext(..., graphic_properties=False)`, the round trip ezdxf's `BaseAttrib.transform` uses), keeping placement, style and height; `_attrib_value` reports the embedded MTEXT's full `Line1\PLine2` content for a multi-line attribute, as AutoCAD's `TextString` does, so get / set / explode / COM agree. Opaque members: `block_explode` iterated `virtual_entities()` without a `skipped_entity_callback`, so ezdxf dropped every member it cannot copy (OLE2FRAME) or transform (VIEWPORT) with a debug log, an ACAD_PROXY_ENTITY without proxy graphics contributed nothing, and the reference was then deleted. `_explodable_members` materializes the list before any write and refuses by member with the typed capability `explode_opaque_members` (declared on both engines: headless False with the reason, COM native), naming the block and the live way out. The block_explode docstrings (backend and tool) now state this refusal instead of claiming the xref/MINSERT gate covered everything. Tests: eight new headless cases in tests/test_track_a_hardening.py -- the multi-line reader, the rewrite (value on every surface, placement unchanged, clean audit), a `\P` value and the empty value, an R2010 save/reload, the single-line path unchanged, the opaque-member refusal (nothing written, reference alive, members named), an ordinary block still exploding under a uniform scale, and the capability declared on both engines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
f2386a9fdd |
fix(task-17): a rotated sheet's margins turn with it, and a paper-units move is named in changed and warnings
Finding 1: page_setup_list swapped size_mm for plot_rotation 1/3 but reported the margins un-turned, so a 297 x 210 sheet carried its portrait margins ([20, 20, 7.5, 7.5], printable 282 x 170) while ezdxf's Page and _paper_frame plotted [7.5, 7.5, 20, 20] (257 x 195). The mapping now lives once in papers.turned_sheet (cross-checked against Page.from_dxf_layout for all four rotations) and is used by the ezdxf row, _paper_frame and the COM row. page_setup_apply resets plot_rotation to 0; headlessly it now re-writes the turned margins un-turned so the sheet keeps the margins it had (nothing moves, changed says nothing), and on COM, where margins come from the .pc3, the ones that moved show up in changed. Finding 2: both engines forced plot paper units to millimetres with no units field in the row, so an inch-authored ANSI sheet was rescaled 25.4x with ok: True and changed naming only the media. Rows now carry paper_units (mm | inches | pixels, DXF group 72 / AcPlotPaperUnits); resolve_page_setup takes paper_units="mm" (default) | "inches" | None (keep the layout's units, as AutoCAD's own Page Setup does); a move is reported in changed and, with its 25.4x consequence and the way back, in the new warnings list (papers.page_setup_warnings, identical on both engines). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
125a8ed3b8 |
feat(settings): SYSVAR_CATALOG + system_variable_describe; system_set_variable refuses out-of-range values
90 authored rows (type, range/enum, default, meaning, saved-in, per-engine support, friendly key); every ezdxf claim is checked against the real backend and the registry-saved set is pinned to what the backend refuses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
4d246b17b8 |
fix(task-9): DIMDSEP refuses control characters and code points above the 16-bit DXF group
check_dim_value's char branch only checked len == 1, so '\n', '\t', '\x00'
and non-BMP characters passed and the headless writer stored ord(value)
unguarded: '\n' split the dimension text over two lines, NUL read back as a
comma, and U+1F600 reached $DIMDSEP / group 278 as 128512, outside the 16-bit
range and refused by AutoCAD as "not a single character". ezdxf's audit
caught none of it. The same hole
|