34079ad0b48e906f27e8b722d39ffa21c842892a
196
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 (
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|