fix: three measurement defects the adversarial triage surfaced
**COM discarded areas it had already read.** `entity_measure` fell back to rebuilding geometry from `.Coordinates` when `.Area` *or* `.Length` was missing, coupling two independent property reads. HATCH, REGION and 3DSOLID expose `.Area` and neither of the others, so the fallback fired, found no vertices, and raised "AutoCAD reported neither an area nor usable vertices" -- while holding the area. Those are the exact types the README sells the COM backend on. Now only a missing `.Area` triggers the fallback; an absent perimeter is reported as `None` with `perimeter_exact: False` instead of being invented, which also removes a `round(None, 6)` TypeError waiting for the first type that answers `.Area` and nothing else. **A plain square hatch reported itself self-intersecting.** `ezdxf.path .flattening()` repeats the start point, and `is_self_intersecting` excluded adjacent edges *by index*, so with the duplicate present the first and last real edges were cross-tested and registered as crossing. Measured: a 40x40 SOLID hatch came back `area: 1600.0, self_intersecting: True`. A closed loop now answers the same whether or not it carries its closing vertex, and a real bowtie is still caught (`area: 0.0, self_intersecting: True`). **The bowtie warning never fired on the live backend.** `self_intersecting` was only computed inside the COM fallback, so an LWPOLYLINE bowtie -- which has both `.Area` and `.Length` and never reaches it -- returned `area: 0.0, self_intersecting: null`. The confident zero this field exists to flag, going out unflagged. It is now computed on the normal path too. Also corrects a docstring written earlier today in `contracts/analysis.py` claiming the live backend "asks AutoCAD for `.Area`, which has already applied the [hatch island] style". Whether ActiveX's hatch area applies the style is not verified against a live seat, and the method could not measure a hatch at all. It now says so. The ActiveX member profiles behind the COM tests are assumptions, not facts, and the tests say so; a live seat settles them. `.Circumference` for CIRCLE and `.Perimeter` for REGION are deliberately NOT added here -- writing more members from the reference is what produced four wrong beliefs in v1.4/v1.5. 1267 passed, 1 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
25f8737132
commit
e24b46d6f0
+28
-13
@@ -931,20 +931,31 @@ class EzdxfBackend(AutoCADBackend):
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
lock = self._lock
|
||||
# One `asyncio.timeout_at` for both phases rather than two
|
||||
# `asyncio.wait_for`s. `wait_for` wraps its awaitable in a Task, so the
|
||||
# old shape cost two extra Tasks and two timer handles on *every* call
|
||||
# — 4,000 of each to draw 2,000 lines, which is where the throughput
|
||||
# this release lost was going. `acquired` keeps the two failure modes
|
||||
# distinguishable, which is the whole reason they were separate.
|
||||
acquired = False
|
||||
call: asyncio.Future | None = None
|
||||
try:
|
||||
await asyncio.wait_for(lock.acquire(), timeout)
|
||||
async with asyncio.timeout_at(deadline):
|
||||
await lock.acquire()
|
||||
acquired = True
|
||||
call = asyncio.ensure_future(asyncio.to_thread(func, *args, **kwargs))
|
||||
result = await call
|
||||
except TimeoutError as exc:
|
||||
raise self._busy_error(timeout) from exc
|
||||
|
||||
call = asyncio.ensure_future(asyncio.to_thread(func, *args, **kwargs))
|
||||
try:
|
||||
result = await asyncio.wait_for(call, max(deadline - loop.time(), 0.0))
|
||||
except TimeoutError as exc:
|
||||
if not call.cancelled():
|
||||
# The call itself raised TimeoutError (an OSError subclass, and
|
||||
# the same type wait_for raises) rather than overrunning the
|
||||
# deadline. That is an ordinary failure: release the lock and
|
||||
# let it through unchanged instead of blaming the deadline.
|
||||
if not acquired:
|
||||
# Timed out queueing behind someone else's live call. That lock
|
||||
# is held by a call that will release it normally; do not
|
||||
# abandon it.
|
||||
raise self._busy_error(timeout) from exc
|
||||
if call is None or not call.cancelled():
|
||||
# The call itself raised TimeoutError (an OSError subclass,
|
||||
# and the same type the deadline raises) rather than overrunning
|
||||
# the deadline. That is an ordinary failure: release the lock
|
||||
# and let it through unchanged instead of blaming the deadline.
|
||||
lock.release()
|
||||
raise
|
||||
if self._lock is lock:
|
||||
@@ -957,7 +968,11 @@ class EzdxfBackend(AutoCADBackend):
|
||||
)
|
||||
raise self._timeout_error(func, timeout) from exc
|
||||
except BaseException:
|
||||
lock.release()
|
||||
# Only ours to release if we ever took it — cancellation can land
|
||||
# while still queueing, and releasing someone else's lock there
|
||||
# would hand the document to two callers at once.
|
||||
if acquired:
|
||||
lock.release()
|
||||
raise
|
||||
lock.release()
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user