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:
Umut CAN
2026-08-06 15:08:50 +03:00
co-authored by Claude Opus 5
parent 25f8737132
commit e24b46d6f0
6 changed files with 242 additions and 19 deletions
+10
View File
@@ -98,7 +98,17 @@ def is_self_intersecting(vertices: Sequence[tuple[float, float]]) -> bool | None
Worth reporting because the shoelace *cancels* crossed lobes rather than
failing: a bowtie made of two 2500-unit triangles measures 0.0, which is a
confident wrong number of exactly the kind this release removes.
A closed loop may arrive either with or without a repeated closing vertex —
`ezdxf.path.flattening()` emits one, raw polyline points do not — and both
spellings must give the same answer. They did not: the adjacency guard
below excludes neighbours *by index*, so with the duplicate present the
first and last real edges were cross-tested even though they share a point,
and every plain hatch boundary came back flagged.
"""
vertices = list(vertices)
if len(vertices) > 1 and math.dist(vertices[0][:2], vertices[-1][:2]) < _EPS:
vertices = vertices[:-1]
count = len(vertices)
if count < 4:
return False