feat(refs): xrefs, raster underlays, data extract and real DWG output

SECTION 24 completes at eleven tools with xref_attach, xref_manage,
image_attach, data_extract and drawing_export_dwg, on the four contract members
that genuinely differ per engine -- backends/contracts/refs.py. Everything else
in the sheet track composes the generic members and is written once.

The ActiveX members were read off the AutoCAD 2026 seat's own type library
(C:\Program Files\Common Files\Autodesk Shared\acax25enu.tlb, "AutoCAD 2025
Type Library" 1.0, the library the running 25.x seat registers) before a line of
the COM path was written, and the measurement is in the code beside the calls:

  IAcadModelSpace.AttachExternalReference(PathName, Name, InsertionPoint,
      Xscale, Yscale, Zscale, Rotation, bOverlay, Password)
  IAcadModelSpace.AddRaster(imageFileName, InsertionPoint, ScaleFactor,
      RotationAngle)
  IAcadBlock: IsXRef (get), Path (get AND put -- so the repath is an assignment,
      not a method), Reload(), Unload(), Bind(bPrefixName), Detach(),
      XRefDatabase (get)
  IAcadDocument.SaveAs(FullFileName, SaveAsType, vSecurityParams)
  AcSaveAsType: ac2000_dwg=12 ac2004_dwg=24 ac2007_dwg=36 ac2010_dwg=48
      ac2013_dwg=60 ac2018_dwg=64 (=acNative)

The running seat itself was not answering COM while this landed (its window was
Not Responding and GetActiveObject never returned), so the type library -- which
is the same seat's declared surface and reads without a call into the process --
is what the members are measured from; no document of the operator's was opened
or touched. This repository has been bitten four times by a fake that invented a
member, so the COM fakes in the tests model those names and nothing else.
ActiveX angles are radians, so the degree-to-radian conversion happens at that
boundary and nowhere else.

Headlessly, ezdxf.xref.attach carries the attachment (measured on ezdxf 1.4.4:
block flags 20 for an attachment and 24 for an overlay, so "is this an xref" is
flags & (BLK_XREF | BLK_XREF_OVERLAY) -- masking BLK_XREF alone misses every
overlay), add_image_def + add_image the raster, with Pillow reading the pixel
size so the placed image keeps the file's aspect.

Three capability keys, declared in both maps:

  dwg_write  -- measured absent here (odafc.is_installed() is False), so the
                ezdxf entry is a runtime re-evaluation like viewport_render's
                matplotlib check rather than a hardcoded False: installing the
                ODA File Converter starts the call working with no edit to the
                map. The refusal names the install route and drawing_export_dxf.
  xref_live  -- reload and bind need a live seat's xref manager and are refused
                headlessly; list, detach and path are headless and are not
                behind the key.
  xlsx_write -- openpyxl is optional and absent here; CSV is always available,
                so the refusal never blocks the deliverable and says so.

Two numbers the plan predicted wrongly are corrected to what the registry
actually does. `block` outranks `drawing` in _GROUP_TAG_PRIORITY, so the two
xref tools file under `blocks` (9 -> 11) beside block_insert -- which is where
an xref belongs, being a block definition that lives in another file -- and
`drawing` moves 11 -> 12, not 11 -> 14. And `data_extract` must not say "parts
list" in its own text: that is the golden discovery query for
entity_create_table, and a file writer outranking the tool that draws the thing
is a regression the tuning bar catches. It is described by its destination --
a file -- instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-22 23:03:57 +03:00
co-authored by Claude Opus 5
parent de8dc8f813
commit 3ef316d520
14 changed files with 1449 additions and 16 deletions
+75 -5
View File
@@ -3,7 +3,7 @@
"schema_version": 1,
"package_version": "1.5.1",
"totals": {
"tools": 210,
"tools": 215,
"resources": 8,
"prompts": 5,
"groups": 23
@@ -11,12 +11,12 @@
"groups": {
"analysis": 12,
"batch": 3,
"blocks": 9,
"blocks": 11,
"corner_ops": 4,
"dimensions": 5,
"drawing": 11,
"engineering": 16,
"entity_creation": 18,
"drawing": 12,
"engineering": 17,
"entity_creation": 19,
"entity_modification": 16,
"entity_query": 9,
"environment": 22,
@@ -414,6 +414,21 @@
"destructiveHint": false
}
},
{
"name": "data_extract",
"group": "engineering",
"cost": "safe",
"summary": "Write the bill of materials to a CSV file, or to XLSX when openpyxl is present.",
"tags": [
"engineering",
"query",
"sheet"
],
"annotations": {
"title": "Sheet: Extract Data",
"destructiveHint": false
}
},
{
"name": "datum_feature",
"group": "engineering",
@@ -697,6 +712,19 @@
"destructiveHint": true
}
},
{
"name": "drawing_export_dwg",
"group": "drawing",
"cost": "safe",
"summary": "Write the drawing as a real DWG at a chosen AutoCAD version.",
"tags": [
"drawing"
],
"annotations": {
"title": "Drawing: Export DWG",
"destructiveHint": false
}
},
{
"name": "drawing_export_dxf",
"group": "drawing",
@@ -1649,6 +1677,20 @@
"destructiveHint": false
}
},
{
"name": "image_attach",
"group": "entity_creation",
"cost": "mutate",
"summary": "Attach a raster image (PNG/JPG) as an underlay.",
"tags": [
"create",
"drawing"
],
"annotations": {
"title": "Sheet: Attach Image",
"destructiveHint": false
}
},
{
"name": "keyway_draw_keyed_bore",
"group": "engineering",
@@ -2988,6 +3030,34 @@
"title": "Set Viewport Scale",
"destructiveHint": false
}
},
{
"name": "xref_attach",
"group": "blocks",
"cost": "mutate",
"summary": "Attach a DWG/DXF as an external reference (attachment or overlay).",
"tags": [
"block",
"drawing"
],
"annotations": {
"title": "Sheet: Attach Xref",
"destructiveHint": false
}
},
{
"name": "xref_manage",
"group": "blocks",
"cost": "mutate",
"summary": "List, reload, bind, detach or repath an external reference.",
"tags": [
"block",
"drawing"
],
"annotations": {
"title": "Sheet: Manage Xrefs",
"destructiveHint": true
}
}
],
"resources": [