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
99 lines
4.0 KiB
Python
99 lines
4.0 KiB
Python
"""External references, raster images and DWG output (v1.6 tracks B+G, group G).
|
|
|
|
These are the only four operations of the sheet track that genuinely differ per
|
|
engine, so they are the only four that become contract members. Everything else
|
|
in `engineering/sheet/` composes the generic members and is written once.
|
|
|
|
``AutoCADBackend`` composes every contract in this package, so importing
|
|
``backends.base.AutoCADBackend`` is unchanged for callers.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
#: An xref is attached (its own xrefs come with it) or overlaid (they do not).
|
|
XREF_KINDS = ("attach", "overlay")
|
|
|
|
#: `list`, `detach` and `path` are headless; `reload` and `bind` need a live
|
|
#: seat and are refused with the `xref_live` capability on the ezdxf engine.
|
|
XREF_ACTIONS = ("list", "reload", "bind", "detach", "path")
|
|
|
|
#: DWG formats both engines name the same way. COM maps them to AcSaveAsType
|
|
#: (measured on AutoCAD 2026: 12/24/36/48/60/64); ezdxf maps them through
|
|
#: `ezdxf.addons.odafc.VERSION_MAP`, which covers exactly these six and more.
|
|
DWG_VERSIONS = ("R2000", "R2004", "R2007", "R2010", "R2013", "R2018")
|
|
|
|
|
|
class RefsContract(ABC):
|
|
@abstractmethod
|
|
async def xref_attach(
|
|
self,
|
|
path: str,
|
|
at,
|
|
scale: float,
|
|
rotation: float,
|
|
kind: str,
|
|
) -> dict:
|
|
"""Attach an external reference and insert it once.
|
|
|
|
The block name is the referenced file's stem, which is what both
|
|
engines do. Returns ``{ok, name, path, kind, handle, at, scale,
|
|
rotation, backend}``.
|
|
|
|
Refuses, before anything is written: a `kind` outside
|
|
:data:`XREF_KINDS`, a file that does not exist, a non-finite
|
|
coordinate/scale/rotation, a non-positive scale, and a block name the
|
|
drawing already holds (re-attaching would either silently replace the
|
|
definition or raise mid-write).
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def xref_manage(self, name: str, action: str, new_path: str | None) -> dict:
|
|
"""One of :data:`XREF_ACTIONS` on one external reference.
|
|
|
|
``list`` ignores `name` and returns
|
|
``{ok, xrefs: [{name, path, kind, inserts}], backend}``. ``path``
|
|
rewrites the saved path to `new_path` and returns the new one.
|
|
``detach`` removes every insert of the xref and its definition, and
|
|
reports ``inserts_removed``. ``reload`` and ``bind`` need a live seat's
|
|
xref manager: the headless engine raises
|
|
``UnsupportedCapabilityError("xref_live", ...)`` rather than pretending,
|
|
which is why the key exists; ``list``/``detach``/``path`` are *not*
|
|
behind it.
|
|
|
|
Refuses an action outside the vocabulary, an unknown xref name, and a
|
|
``path`` action with no `new_path`.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def image_attach(self, path: str, at, scale: float, rotation: float) -> dict:
|
|
"""Attach a raster image as an underlay.
|
|
|
|
`scale` is drawing units per pixel, so the placed size is the image's
|
|
pixel size times `scale` and the aspect ratio is the file's. Returns
|
|
``{ok, path, handle, at, scale, rotation, pixels: [w, h],
|
|
size_mm: [w, h], backend}``.
|
|
|
|
Refuses a file that does not exist, a file no image reader can open
|
|
(naming it), a non-positive scale and a non-finite number -- all before
|
|
anything is written.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def drawing_export_dwg(self, path: str, version: str) -> dict:
|
|
"""Write the current drawing as a real DWG.
|
|
|
|
Returns ``{ok, path, version, bytes, backend}``. Refuses a `version`
|
|
outside :data:`DWG_VERSIONS` by name. Headlessly this needs the ODA
|
|
File Converter and otherwise raises
|
|
``UnsupportedCapabilityError("dwg_write", ...)`` naming the install
|
|
route; on a live seat it is ``Document.SaveAs`` with the matching
|
|
``AcSaveAsType``, which -- as AutoCAD's own SaveAs does -- leaves the
|
|
session bound to the file it just wrote.
|
|
"""
|
|
...
|