fix(task-14): arcs past a semicircle and foreign ports behind tag text
Two verified blocking findings on pid_graph. `_point_segment_distance` put the centre of an arc larger than a semicircle (|bulge| > 1) on the wrong side of the chord: `copysign` discarded the sign of `radius - sagitta`, which is exactly what carries the centre across the chord past 180 degrees. A bulge-2 arc was tested as its own reflection, so a line ending on its true apex dangled and one ending on empty paper made a junction. The offset now keeps that sign and only takes the bulge's direction from the bulge. Foreign-block port inference tested line ends against the INSERT's `bounding_box`, which on both engines takes the ATTRIB text with it. A TAG lettered above a valve pushed the box past the body, so a line ending on the body's real edge on that side was *inside* the box and reported dangling. Both engines now report `geometry_bbox` for an INSERT - the drawn geometry only, ATTRIB/ATTDEF excluded, in WCS, omitted when the block draws nothing (ezdxf via `virtual_entities`, COM by carrying each definition member's box through the INSERT's origin, scale and rotation). The graph prefers it over `bounding_box`, counts an end as touching when it is within tolerance of the box or inside it, and when several foreign boxes hold the end picks the nearest insertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b6f355c3d
commit
d17a26103f
@@ -348,6 +348,54 @@ def _com_bulges(entity, coords, closed: bool, length) -> list[float]:
|
||||
return [_com_bulge(entity, i) for i in range(count)]
|
||||
|
||||
|
||||
def _com_geometry_bbox(entity) -> dict | None:
|
||||
"""WCS extents of a block reference's *drawn* geometry, attributes excluded.
|
||||
|
||||
``GetBoundingBox`` on an INSERT takes its ATTRIBs with it, so a TAG lettered
|
||||
above a valve pushes the box past the body — a line ending on the body's
|
||||
real edge then lies *inside* the box. The definition's members are read in
|
||||
block space (skipping ``AcDbAttributeDefinition``), each member's box is
|
||||
carried through the INSERT (origin, scale, rotation) corner by corner, and
|
||||
the extents of those corners is the answer. None when there is nothing
|
||||
drawn or ActiveX refuses any step — the caller then omits the key.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
doc = entity.Document
|
||||
except Exception:
|
||||
doc = _acad_doc()
|
||||
blk = doc.Blocks.Item(entity.Name)
|
||||
try:
|
||||
origin = tuple(blk.Origin)
|
||||
except Exception:
|
||||
origin = (0.0, 0.0, 0.0)
|
||||
ins = entity.InsertionPoint
|
||||
sx, sy = float(entity.XScaleFactor), float(entity.YScaleFactor)
|
||||
rot = float(entity.Rotation)
|
||||
cos_r, sin_r = math.cos(rot), math.sin(rot)
|
||||
xs: list[float] = []
|
||||
ys: list[float] = []
|
||||
for i in range(blk.Count):
|
||||
member = blk.Item(i)
|
||||
if member.ObjectName == "AcDbAttributeDefinition":
|
||||
continue
|
||||
try:
|
||||
lo, hi = member.GetBoundingBox()
|
||||
except Exception:
|
||||
continue
|
||||
for bx, by in ((lo[0], lo[1]), (hi[0], lo[1]), (lo[0], hi[1]), (hi[0], hi[1])):
|
||||
lx = (float(bx) - float(origin[0])) * sx
|
||||
ly = (float(by) - float(origin[1])) * sy
|
||||
xs.append(float(ins[0]) + lx * cos_r - ly * sin_r)
|
||||
ys.append(float(ins[1]) + lx * sin_r + ly * cos_r)
|
||||
if not xs:
|
||||
return None
|
||||
return {"min": [min(xs), min(ys)], "max": [max(xs), max(ys)]}
|
||||
except Exception as exc:
|
||||
log.debug("geometry bbox failed for block reference: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
# What a lightweight polyline calls itself over ActiveX. The live name is
|
||||
# ``AcDbPolyline`` (measured on AutoCAD 2026, 2026-08-06 — the member profile in
|
||||
# tests/test_com_backend.py); ``AcDbLWPolyline`` is the DXF-flavoured spelling
|
||||
@@ -449,6 +497,9 @@ def _entity_info(entity) -> EntityInfo:
|
||||
props["x_scale"] = entity.XScaleFactor
|
||||
props["y_scale"] = entity.YScaleFactor
|
||||
props["rotation_deg"] = rad2deg(entity.Rotation)
|
||||
geometry_bbox = _com_geometry_bbox(entity)
|
||||
if geometry_bbox is not None:
|
||||
props["geometry_bbox"] = geometry_bbox
|
||||
except Exception as exc:
|
||||
log.debug("Type-specific entity properties extraction failed: %s", exc)
|
||||
|
||||
|
||||
@@ -845,6 +845,24 @@ def _entity_info_dxf(ent) -> EntityInfo:
|
||||
props["x_scale"] = ent.dxf.xscale if ent.dxf.hasattr("xscale") else 1.0
|
||||
props["y_scale"] = ent.dxf.yscale if ent.dxf.hasattr("yscale") else 1.0
|
||||
props["rotation_deg"] = ent.dxf.get("rotation", 0.0)
|
||||
# The drawn symbol alone. ``bounding_box`` below takes the ATTRIBs
|
||||
# with it, so a TAG above a valve pushes the box past the body and
|
||||
# a line ending on the body's real edge reads as *inside* it.
|
||||
# `virtual_entities` already applies the INSERT transform, so this
|
||||
# is WCS; the key is omitted when the block draws nothing.
|
||||
try:
|
||||
from ezdxf import bbox as _bbox
|
||||
|
||||
_gb = _bbox.extents(
|
||||
e for e in ent.virtual_entities() if e.dxftype() not in ("ATTRIB", "ATTDEF")
|
||||
)
|
||||
if _gb.has_data:
|
||||
props["geometry_bbox"] = {
|
||||
"min": [float(_gb.extmin.x), float(_gb.extmin.y)],
|
||||
"max": [float(_gb.extmax.x), float(_gb.extmax.y)],
|
||||
}
|
||||
except Exception as exc:
|
||||
log.debug("geometry bbox failed for INSERT %s: %s", ent.dxf.name, exc)
|
||||
elif ent_type == "ELLIPSE":
|
||||
props["center"] = _v2(ent.dxf.center)
|
||||
props["major_axis"] = _v2(ent.dxf.major_axis)
|
||||
|
||||
Reference in New Issue
Block a user