34079ad0b48e906f27e8b722d39ffa21c842892a
154
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |