fix(correctness-bundle): I16 correctness fixes + dead code removal + ruff clean
N1: COM hatch non-associative (AddHatch associativity=False) N2: ezdxf refuses DWG saves with clear error; server.py derives fmt from extension R19: dimension_aligned dist→distance; dimension_angular correct add_angular_dim_2l args (base/line1/line2/location) R9: polar array 360° no-duplicate (divisor=n when fill_angle%360<ε) R13: entity_offset side_x/side_y honored in ezdxf via cross-product (LINE) and distance-to-center (CIRCLE) R18: drawing_new bootstrap failure surfaces status="degraded" instead of swallowing silently Dead code: engineering/section.py, gear.generate_tooth_profile, CommandResult, _ACI_NAMES, set_layer_active, ensure_linetypes, ComBackend._batch_mode instance attr, engineering/__init__ build-seam comment Ruff: 38 auto-fixed + 5 manual → codebase is now ruff-clean; CI lint is now blocking 184 tests pass.
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
"""AutoCAD MCP Pro – backend package."""
|
||||
from .base import AutoCADBackend, BlockInfo, CommandResult, DrawingInfo, EntityInfo, LayerInfo
|
||||
from .base import AutoCADBackend, BlockInfo, DrawingInfo, EntityInfo, LayerInfo
|
||||
from .com_backend import ComBackend
|
||||
from .ezdxf_backend import EzdxfBackend
|
||||
|
||||
__all__ = [
|
||||
"AutoCADBackend",
|
||||
"CommandResult",
|
||||
"EntityInfo",
|
||||
"LayerInfo",
|
||||
"BlockInfo",
|
||||
|
||||
+13
-54
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from engineering.plan_spec import (
|
||||
@@ -23,21 +23,6 @@ if TYPE_CHECKING:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandResult:
|
||||
ok: bool
|
||||
payload: Any = None
|
||||
error: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d: dict[str, Any] = {"ok": self.ok}
|
||||
if self.ok:
|
||||
d["payload"] = self.payload
|
||||
else:
|
||||
d["error"] = self.error
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityInfo:
|
||||
handle: str
|
||||
@@ -91,7 +76,7 @@ class DrawingInfo:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def normalize_lineweight(lw: "float | int | None") -> "int | None":
|
||||
def normalize_lineweight(lw: float | int | None) -> int | None:
|
||||
"""Coerce a lineweight to ezdxf/COM hundredths-of-a-mm integers.
|
||||
|
||||
AutoCAD and ezdxf store lineweights as integer hundredths (25 == 0.25 mm),
|
||||
@@ -548,17 +533,17 @@ class AutoCADBackend(ABC):
|
||||
# behaviour. The only backend-specific primitive is `_create_xline` — XLINE
|
||||
# has no cross-backend equivalent among the standard entity_create_* tools.
|
||||
|
||||
_plan_spec: "PlanSpec | None" = None
|
||||
_plan_spec: PlanSpec | None = None
|
||||
|
||||
async def drawing_plan(
|
||||
self, intent: str,
|
||||
sheet_size: "SheetSize" = "A3",
|
||||
sheet_size: SheetSize = "A3",
|
||||
scale: float = 1.0,
|
||||
layer_set_id: "LayerSetId" = "mech",
|
||||
layer_set_id: LayerSetId = "mech",
|
||||
view_count: int = 1,
|
||||
dim_style: "DimStyle" = "chain",
|
||||
dim_style: DimStyle = "chain",
|
||||
notes: list[str] | None = None,
|
||||
) -> "PlanSpec":
|
||||
) -> PlanSpec:
|
||||
"""Commit a drawing intent before any geometry is created.
|
||||
Returns a PlanSpec held on the backend for `drawing_critique`."""
|
||||
from engineering.plan_spec import PlanSpec
|
||||
@@ -575,15 +560,15 @@ class AutoCADBackend(ABC):
|
||||
return plan
|
||||
|
||||
async def drawing_critique(
|
||||
self, focus: list["CritiqueFocus"] | None = None,
|
||||
) -> list["Issue"]:
|
||||
self, focus: list[CritiqueFocus] | None = None,
|
||||
) -> list[Issue]:
|
||||
"""Run premium-quality checks; return zero issues before
|
||||
`drawing_finalize`. `focus=None` runs all checks."""
|
||||
from engineering.critique import run_critique
|
||||
return await run_critique(self, focus)
|
||||
|
||||
async def point_from_snap(
|
||||
self, handle: str, snap: "SnapType",
|
||||
self, handle: str, snap: SnapType,
|
||||
ref_x: float | None = None, ref_y: float | None = None,
|
||||
) -> tuple[float, float]:
|
||||
"""Return a deterministic snap point on `handle`.
|
||||
@@ -769,7 +754,7 @@ class AutoCADBackend(ABC):
|
||||
return {"ok": True, "layer": layer, "deleted": deleted}
|
||||
|
||||
async def drawing_apply_iso_layers(
|
||||
self, standard: "LayerSetId" = "mech",
|
||||
self, standard: LayerSetId = "mech",
|
||||
) -> dict:
|
||||
"""Bootstrap a full ISO-conformant layer set with correct colors
|
||||
and lineweights. Idempotent."""
|
||||
@@ -778,7 +763,7 @@ class AutoCADBackend(ABC):
|
||||
return {"ok": True, "standard": standard, "layers": result}
|
||||
|
||||
async def dimension_auto(
|
||||
self, handles: list[str], style: "DimStyle" = "chain",
|
||||
self, handles: list[str], style: DimStyle = "chain",
|
||||
offset: float = 10.0,
|
||||
) -> list[EntityInfo]:
|
||||
"""Generate ISO 129 dimensions across `handles` in the chosen style
|
||||
@@ -828,7 +813,7 @@ class AutoCADBackend(ABC):
|
||||
dx, dy = ex0 - sx0, ey0 - sy0
|
||||
L0 = math.hypot(dx, dy) or 1.0
|
||||
ux, uy = dx / L0, dy / L0
|
||||
for i, (sx, sy, ex, ey) in enumerate(segs):
|
||||
for i, (_sx, _sy, ex, ey) in enumerate(segs):
|
||||
dim = await self.dimension_linear(
|
||||
sx0, sy0, ex, ey,
|
||||
sx0 - uy * (off + i * off), sy0 + ux * (off + i * off),
|
||||
@@ -906,32 +891,6 @@ class AutoCADBackend(ABC):
|
||||
|
||||
return [e for e in ents if _ok(e)]
|
||||
|
||||
# ── concrete helpers (use existing primitives) ──────────────────────────
|
||||
async def set_layer_active(self, name: str) -> None:
|
||||
"""Convenience: alias for layer_set_current. Concrete; backends need not override."""
|
||||
await self.layer_set_current(name)
|
||||
|
||||
async def ensure_linetypes(
|
||||
self, names: list[str], file: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Idempotently load each linetype in `names`. Returns {name: status}.
|
||||
Status is one of: 'already_loaded', 'loaded', 'failed: <msg>'.
|
||||
"""
|
||||
try:
|
||||
current = {ln.lower() for ln in await self.linetype_list()}
|
||||
except Exception:
|
||||
current = set()
|
||||
results: dict[str, str] = {}
|
||||
for n in names:
|
||||
if n.lower() in current:
|
||||
results[n] = "already_loaded"
|
||||
continue
|
||||
try:
|
||||
await self.linetype_load(n, file=file)
|
||||
results[n] = "loaded"
|
||||
except Exception as exc:
|
||||
results[n] = f"failed: {exc}"
|
||||
return results
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -360,7 +360,6 @@ class ComBackend(AutoCADBackend):
|
||||
|
||||
def __init__(self):
|
||||
self._executor: ThreadPoolExecutor | None = None
|
||||
self._batch_mode = False
|
||||
self._connected = False
|
||||
self._transaction_active = False
|
||||
|
||||
@@ -413,7 +412,7 @@ class ComBackend(AutoCADBackend):
|
||||
if timeout > 0:
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
return await future
|
||||
except asyncio.TimeoutError as e:
|
||||
except TimeoutError as e:
|
||||
log.error("COM call timed out after %.1fs; rebuilding executor", timeout)
|
||||
# The worker thread is still blocked inside SendCommand and cannot be
|
||||
# cancelled. Abandon it (shutdown(wait=False)) and start a fresh
|
||||
@@ -656,7 +655,7 @@ class ComBackend(AutoCADBackend):
|
||||
def _sync():
|
||||
mspace = _msp()
|
||||
# acPatternTypePreDefined = 0
|
||||
hatch = mspace.AddHatch(0, pattern, True)
|
||||
hatch = mspace.AddHatch(0, pattern, False)
|
||||
hatch.PatternScale = float(scale)
|
||||
hatch.PatternAngle = deg2rad(angle)
|
||||
# Build outer loop as a temporary lwpolyline
|
||||
@@ -886,7 +885,6 @@ class ComBackend(AutoCADBackend):
|
||||
self, handle, rows, cols, row_spacing, col_spacing,
|
||||
) -> list[EntityInfo]:
|
||||
def _sync():
|
||||
self._batch_mode = True
|
||||
_COM_STATE["batch_mode"] = True
|
||||
try:
|
||||
doc = _acad_doc()
|
||||
@@ -904,7 +902,6 @@ class ComBackend(AutoCADBackend):
|
||||
entities.append(_entity_info(result))
|
||||
return entities
|
||||
finally:
|
||||
self._batch_mode = False
|
||||
_COM_STATE["batch_mode"] = False
|
||||
_regen()
|
||||
return await self._run(_sync)
|
||||
@@ -913,7 +910,6 @@ class ComBackend(AutoCADBackend):
|
||||
self, handle, count, fill_angle, center_x, center_y,
|
||||
) -> list[EntityInfo]:
|
||||
def _sync():
|
||||
self._batch_mode = True
|
||||
_COM_STATE["batch_mode"] = True
|
||||
try:
|
||||
doc = _acad_doc()
|
||||
@@ -930,7 +926,6 @@ class ComBackend(AutoCADBackend):
|
||||
entities.append(_entity_info(result))
|
||||
return entities
|
||||
finally:
|
||||
self._batch_mode = False
|
||||
_COM_STATE["batch_mode"] = False
|
||||
_regen()
|
||||
return await self._run(_sync)
|
||||
|
||||
+38
-19
@@ -51,11 +51,6 @@ except ImportError:
|
||||
# Helper utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ACI_NAMES = {
|
||||
1: "Red", 2: "Yellow", 3: "Green", 4: "Cyan",
|
||||
5: "Blue", 6: "Magenta", 7: "White", 256: "ByLayer", 0: "ByBlock",
|
||||
}
|
||||
|
||||
_BUILTIN_LINETYPES = {"continuous", "bylayer", "byblock"}
|
||||
|
||||
# AutoCAD-shipped linetypes that ezdxf.tools.standards does not include.
|
||||
@@ -344,12 +339,17 @@ class EzdxfBackend(AutoCADBackend):
|
||||
return await self._async(_sync)
|
||||
|
||||
async def drawing_save_as(self, path: str, fmt: str = "dxf") -> dict:
|
||||
if fmt.lower() == "dwg":
|
||||
raise RuntimeError(
|
||||
"ezdxf backend cannot write true DWG binary. "
|
||||
"Use save_path with a .dxf extension, or switch to the COM backend."
|
||||
)
|
||||
def _sync():
|
||||
doc = self._require_doc()
|
||||
doc.saveas(path)
|
||||
self._doc_path = path
|
||||
self._dirty = False
|
||||
return {"ok": True, "path": path, "format": fmt}
|
||||
return {"ok": True, "path": path, "format": "dxf"}
|
||||
return await self._async(_sync)
|
||||
|
||||
async def drawing_export_dxf(self, path: str) -> dict:
|
||||
@@ -693,7 +693,7 @@ class EzdxfBackend(AutoCADBackend):
|
||||
dim = msp.add_aligned_dim(
|
||||
p1=(float(x1), float(y1)),
|
||||
p2=(float(x2), float(y2)),
|
||||
dist=math.sqrt((float(dim_x) - float(x1)) ** 2 + (float(dim_y) - float(y1)) ** 2),
|
||||
distance=math.sqrt((float(dim_x) - float(x1)) ** 2 + (float(dim_y) - float(y1)) ** 2),
|
||||
)
|
||||
dim.render()
|
||||
ent = dim.dimension
|
||||
@@ -708,11 +708,12 @@ class EzdxfBackend(AutoCADBackend):
|
||||
) -> EntityInfo:
|
||||
def _sync():
|
||||
msp = self._msp()
|
||||
vxf, vyf = float(vx), float(vy)
|
||||
dim = msp.add_angular_dim_2l(
|
||||
center=(float(vx), float(vy)),
|
||||
p1=(float(x1), float(y1)),
|
||||
p2=(float(x2), float(y2)),
|
||||
distance=10.0,
|
||||
base=(float(tx), float(ty)),
|
||||
line1=((vxf, vyf), (float(x1), float(y1))),
|
||||
line2=((vxf, vyf), (float(x2), float(y2))),
|
||||
location=(float(tx), float(ty)),
|
||||
)
|
||||
dim.render()
|
||||
ent = dim.dimension
|
||||
@@ -875,11 +876,19 @@ class EzdxfBackend(AutoCADBackend):
|
||||
d = float(distance)
|
||||
|
||||
if ent_type == "LINE":
|
||||
# Offset a line: move parallel
|
||||
# Offset a line: move parallel; side_x/side_y selects which side
|
||||
start = Vec2(ent.dxf.start.x, ent.dxf.start.y)
|
||||
end = Vec2(ent.dxf.end.x, ent.dxf.end.y)
|
||||
direction = (end - start).normalize()
|
||||
normal = Vec2(-direction.y, direction.x) * d
|
||||
left_normal = Vec2(-direction.y, direction.x)
|
||||
# Determine sign: positive = left side (default); flip if side point is on right
|
||||
sign = 1.0
|
||||
if side_x is not None and side_y is not None:
|
||||
mid = (start + end) * 0.5
|
||||
side_vec = Vec2(float(side_x), float(side_y)) - mid
|
||||
if left_normal.dot(side_vec) < 0:
|
||||
sign = -1.0
|
||||
normal = left_normal * (d * sign)
|
||||
new_start = start + normal
|
||||
new_end = end + normal
|
||||
msp = self._msp()
|
||||
@@ -893,7 +902,13 @@ class EzdxfBackend(AutoCADBackend):
|
||||
|
||||
elif ent_type == "CIRCLE":
|
||||
cx, cy = ent.dxf.center.x, ent.dxf.center.y
|
||||
new_r = ent.dxf.radius + d
|
||||
# side_x/side_y selects inside (shrink) or outside (grow); default = outside
|
||||
sign = 1.0
|
||||
if side_x is not None and side_y is not None:
|
||||
dist_to_center = math.sqrt((float(side_x) - cx) ** 2 + (float(side_y) - cy) ** 2)
|
||||
if dist_to_center < ent.dxf.radius:
|
||||
sign = -1.0
|
||||
new_r = ent.dxf.radius + d * sign
|
||||
if new_r <= 0:
|
||||
raise ValueError("Offset distance too large for circle")
|
||||
msp = self._msp()
|
||||
@@ -944,8 +959,12 @@ class EzdxfBackend(AutoCADBackend):
|
||||
from ezdxf.math import Matrix44
|
||||
results = []
|
||||
cx, cy = float(center_x), float(center_y)
|
||||
step = math.radians(float(fill_angle)) / max(int(count) - 1, 1)
|
||||
for i in range(1, int(count)):
|
||||
fa = float(fill_angle)
|
||||
n = int(count)
|
||||
# 360° full circle: n copies evenly spaced, no duplicate at 0°
|
||||
divisor = n if abs(fa % 360.0) < 1e-6 else max(n - 1, 1)
|
||||
step = math.radians(fa) / divisor
|
||||
for i in range(1, n):
|
||||
copy = ent.copy()
|
||||
msp.add_entity(copy)
|
||||
angle = step * i
|
||||
@@ -1535,7 +1554,7 @@ class EzdxfBackend(AutoCADBackend):
|
||||
|
||||
async def entity_trim(self, target_handle, cutter_handle, keep_x, keep_y) -> EntityInfo:
|
||||
def _sync():
|
||||
from ezdxf.math import intersection_line_line_2d, Vec2
|
||||
from ezdxf.math import Vec2, intersection_line_line_2d
|
||||
target = self._get_entity(target_handle)
|
||||
cutter = self._get_entity(cutter_handle)
|
||||
if target.dxftype() != "LINE" or cutter.dxftype() != "LINE":
|
||||
@@ -1574,7 +1593,7 @@ class EzdxfBackend(AutoCADBackend):
|
||||
self, target_handle, boundary_handle, end_x=None, end_y=None,
|
||||
) -> EntityInfo:
|
||||
def _sync():
|
||||
from ezdxf.math import intersection_line_line_2d, Vec2
|
||||
from ezdxf.math import Vec2, intersection_line_line_2d
|
||||
target = self._get_entity(target_handle)
|
||||
boundary = self._get_entity(boundary_handle)
|
||||
if target.dxftype() != "LINE" or boundary.dxftype() != "LINE":
|
||||
@@ -1616,7 +1635,7 @@ class EzdxfBackend(AutoCADBackend):
|
||||
"""Shared geometry setup for fillet/chamfer.
|
||||
Returns (line1, line2, P, d1, d2, A_far, B_far, theta) where d1/d2 are
|
||||
unit vectors from intersection P toward the far endpoint of each line."""
|
||||
from ezdxf.math import intersection_line_line_2d, Vec2
|
||||
from ezdxf.math import Vec2, intersection_line_line_2d
|
||||
line1 = self._get_entity(handle1)
|
||||
line2 = self._get_entity(handle2)
|
||||
if line1.dxftype() != "LINE" or line2.dxftype() != "LINE":
|
||||
|
||||
Reference in New Issue
Block a user