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:
Umutcan Edizsalan
2026-06-18 11:34:30 +03:00
parent 702af0d6e1
commit 0c7f64ceb6
51 changed files with 2494 additions and 2865 deletions
+2 -21
View File
@@ -6,7 +6,6 @@ from .gear import (
draw_spur_gear_front_view,
generate_full_gear_outline,
generate_involute_flank,
generate_tooth_profile,
involute_xy,
)
from .keyway import (
@@ -32,23 +31,8 @@ from .plan_spec import (
SheetSize,
SnapType,
)
from .section import apply_section_hatch, cutting_plane_line
# Agent B contributes titleblock + validator. Imports are guarded so this
# package remains usable while B is still in flight; once those modules land
# the symbols flow through transparently.
try:
from .titleblock import TitleBlockMetadata, apply_iso_a3_titleblock
except ImportError:
TitleBlockMetadata = None # type: ignore[assignment]
apply_iso_a3_titleblock = None # type: ignore[assignment]
try:
from .validator import DrawingValidator, ValidationFinding, ValidationResult
except ImportError:
DrawingValidator = None # type: ignore[assignment]
ValidationFinding = None # type: ignore[assignment]
ValidationResult = None # type: ignore[assignment]
from .titleblock import TitleBlockMetadata, apply_iso_a3_titleblock
from .validator import DrawingValidator, ValidationFinding, ValidationResult
__all__ = [
"draw_helical_gear_front_view",
@@ -56,14 +40,11 @@ __all__ = [
"draw_gear_section_aa",
"generate_full_gear_outline",
"generate_involute_flank",
"generate_tooth_profile",
"involute_xy",
"draw_keyed_bore",
"draw_keyway_section",
"keyway_dimensions",
"DIN6885_TABLE",
"cutting_plane_line",
"apply_section_hatch",
"ENGINEERING_LAYERS",
"STANDARD_LINETYPES",
"ensure_engineering_layers",
+7 -7
View File
@@ -34,7 +34,7 @@ _DIM_TYPES = {
}
async def _check_iso128(backend: "AutoCADBackend") -> list[Issue]:
async def _check_iso128(backend: AutoCADBackend) -> list[Issue]:
"""Layer lineweights must be in ISO_128_LINEWEIGHTS_MM (or -3=ByLayer
sentinel meaning 'inherit')."""
issues: list[Issue] = []
@@ -69,7 +69,7 @@ async def _check_iso128(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_layer_color(backend: "AutoCADBackend") -> list[Issue]:
async def _check_layer_color(backend: AutoCADBackend) -> list[Issue]:
"""Engineering-standard layers must keep their canonical ACI color."""
issues: list[Issue] = []
try:
@@ -93,7 +93,7 @@ async def _check_layer_color(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_construction_left(backend: "AutoCADBackend") -> list[Issue]:
async def _check_construction_left(backend: AutoCADBackend) -> list[Issue]:
"""CONSTRUCTION layer must be empty before drawing_finalize."""
try:
ents = await backend.entity_list(layer_filter="CONSTRUCTION", limit=5000)
@@ -113,7 +113,7 @@ async def _check_construction_left(backend: "AutoCADBackend") -> list[Issue]:
)]
async def _check_untrimmed_corner(backend: "AutoCADBackend") -> list[Issue]:
async def _check_untrimmed_corner(backend: AutoCADBackend) -> list[Issue]:
"""Two LINE endpoints within `tol` but not exactly equal usually means a
corner where one line overshoots — TRIM/EXTEND/FILLET was forgotten."""
tol = 0.5 # mm — generous; tighten via PlanSpec.notes later
@@ -156,7 +156,7 @@ async def _check_untrimmed_corner(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_duplicate_entities(backend: "AutoCADBackend") -> list[Issue]:
async def _check_duplicate_entities(backend: AutoCADBackend) -> list[Issue]:
"""Two LINEs with identical endpoints (in either direction) are duplicates."""
try:
lines = await backend.entity_list(type_filter="LINE", limit=5000)
@@ -185,7 +185,7 @@ async def _check_duplicate_entities(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_dim_overlap(backend: "AutoCADBackend") -> list[Issue]:
async def _check_dim_overlap(backend: AutoCADBackend) -> list[Issue]:
"""Two dimensions whose insertion points are within `tol` likely overlap."""
tol = 5.0 # mm
try:
@@ -232,7 +232,7 @@ _FOCUS_DISPATCH = {
async def run_critique(
backend: "AutoCADBackend",
backend: AutoCADBackend,
focus: list[CritiqueFocus] | None = None,
) -> list[Issue]:
"""Dispatch each requested focus check and return aggregated issues."""
+4 -56
View File
@@ -71,58 +71,6 @@ def _arc_points(
return pts
def generate_tooth_profile(
module: float,
teeth: int,
pressure_angle: float = 20.0,
n_points_per_flank: int = 40,
) -> list[tuple[float, float]]:
"""Single tooth profile: right flank, tip arc, mirrored left flank."""
pa = math.radians(pressure_angle)
pitch_r = module * teeth / 2.0
base_r = pitch_r * math.cos(pa)
outer_r = pitch_r + module
root_r = pitch_r - 1.25 * module
# Tooth half-thickness angle on the pitch circle.
s = math.pi * module / 2.0
theta_pitch = s / pitch_r / 2.0
# Find involute parameter at pitch circle so we can shift the flank to be
# symmetric about the tooth centerline (y=0 axis after rotation).
if outer_r > base_r:
t_pitch = math.sqrt((pitch_r / base_r) ** 2 - 1.0) if pitch_r > base_r else 0.0
else:
t_pitch = 0.0
inv_pitch = math.tan(pa) - pa # involute function at pressure angle
# The flank generated by `generate_involute_flank` starts at theta=0 on the
# base circle. The involute point at the pitch radius makes angle inv_pitch
# (approx). We rotate the flank by -inv_pitch + theta_pitch so the pitch
# crossing sits at +theta_pitch above the tooth-center axis.
rot = -inv_pitch + theta_pitch
right_flank = generate_involute_flank(base_r, root_r, outer_r, n_points_per_flank)
right_flank = _rotate_points(right_flank, rot)
# Left flank by mirroring across the X axis (tooth centerline).
left_flank = [(x, -y) for x, y in right_flank]
# Tip arc connects the outermost right-flank point to the outermost left-flank point.
tip_right = right_flank[-1]
tip_left = left_flank[-1]
a_right = math.atan2(tip_right[1], tip_right[0])
a_left = math.atan2(tip_left[1], tip_left[0])
if a_left > a_right:
a_left -= 2 * math.pi
tip_arc = _arc_points(0.0, 0.0, outer_r, a_right, a_left, n=5)
profile: list[tuple[float, float]] = []
profile.extend(right_flank)
profile.extend(tip_arc[1:-1])
profile.extend(reversed(left_flank))
return profile
def generate_full_gear_outline(
module: float,
teeth: int,
@@ -200,7 +148,7 @@ def generate_full_gear_outline(
async def _draw_helix_symbol(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
cx: float,
cy: float,
@@ -237,7 +185,7 @@ async def _draw_helix_symbol(
async def draw_helical_gear_front_view(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
module: float,
teeth: int,
@@ -330,7 +278,7 @@ async def draw_helical_gear_front_view(
async def draw_spur_gear_front_view(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
module: float,
teeth: int,
@@ -356,7 +304,7 @@ async def draw_spur_gear_front_view(
async def draw_gear_section_aa(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
gear_metadata: dict,
x_offset: float,
+2 -2
View File
@@ -56,7 +56,7 @@ def keyway_dimensions(bore_diameter: float) -> dict:
async def draw_keyed_bore(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
center: tuple[float, float],
bore_diameter: float,
@@ -110,7 +110,7 @@ async def draw_keyed_bore(
async def draw_keyway_section(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
center: tuple[float, float],
bore_diameter: float,
+3 -3
View File
@@ -71,7 +71,7 @@ LAYER_SET_REGISTRY: dict[str, list[tuple[str, int, str, float, str]]] = {
STANDARD_LINETYPES: list[str] = ["CENTER", "HIDDEN", "PHANTOM", "DASHED", "DASHDOT"]
async def ensure_standard_linetypes(backend: "AutoCADBackend") -> dict[str, str]:
async def ensure_standard_linetypes(backend: AutoCADBackend) -> dict[str, str]:
"""Idempotently load STANDARD_LINETYPES via backend.linetype_load."""
try:
existing = {ln.lower() for ln in await backend.linetype_list()}
@@ -92,13 +92,13 @@ async def ensure_standard_linetypes(backend: "AutoCADBackend") -> dict[str, str]
return results
async def ensure_engineering_layers(backend: "AutoCADBackend") -> dict[str, str]:
async def ensure_engineering_layers(backend: AutoCADBackend) -> dict[str, str]:
"""Idempotently create every layer in ENGINEERING_LAYERS via backend.layer_create."""
return await apply_layer_set(backend, "mech")
async def apply_layer_set(
backend: "AutoCADBackend", standard: str = "mech",
backend: AutoCADBackend, standard: str = "mech",
) -> dict[str, str]:
"""Idempotently apply a named layer set ('mech', 'pid', 'iso13567')."""
layer_set = LAYER_SET_REGISTRY.get(standard)
-82
View File
@@ -1,82 +0,0 @@
"""Generic cutting-plane and section-hatch helpers."""
from __future__ import annotations
import math
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from backends.base import AutoCADBackend
async def cutting_plane_line(
backend: "AutoCADBackend",
*,
p1: tuple[float, float],
p2: tuple[float, float],
label: str = "A",
layer: str = "PHANTOM",
) -> dict:
"""Draw a section line, arrowheads at each end, and 'X-X' labels."""
x1, y1 = float(p1[0]), float(p1[1])
x2, y2 = float(p2[0]), float(p2[1])
line = await backend.entity_create_line(x1, y1, x2, y2, layer=layer)
dx, dy = x2 - x1, y2 - y1
length = math.hypot(dx, dy) or 1.0
ux, uy = dx / length, dy / length
nx, ny = -uy, ux
arrow_len = max(length * 0.05, 4.0)
arrow_w = arrow_len * 0.4
arrows: list[str] = []
for tip_x, tip_y, dir_x, dir_y in (
(x1, y1, -ux, -uy),
(x2, y2, ux, uy),
):
bx = tip_x + dir_x * arrow_len
by = tip_y + dir_y * arrow_len
wing1_x = bx + nx * arrow_w
wing1_y = by + ny * arrow_w
wing2_x = bx - nx * arrow_w
wing2_y = by - ny * arrow_w
a1 = await backend.entity_create_line(tip_x, tip_y, wing1_x, wing1_y, layer=layer)
a2 = await backend.entity_create_line(tip_x, tip_y, wing2_x, wing2_y, layer=layer)
arrows.extend([a1.handle, a2.handle])
text = f"{label}-{label}"
text_h = max(arrow_len, 3.5)
offset = arrow_len * 1.5
labels: list[str] = []
for tip_x, tip_y, dir_x, dir_y in (
(x1, y1, -ux, -uy),
(x2, y2, ux, uy),
):
tx = tip_x + dir_x * offset + nx * offset * 0.5
ty = tip_y + dir_y * offset + ny * offset * 0.5
t = await backend.entity_create_text(text, tx, ty, height=text_h, layer="TEXT")
labels.append(t.handle)
return {"line": line.handle, "arrows": arrows, "labels": labels}
async def apply_section_hatch(
backend: "AutoCADBackend",
*,
boundary_points: list[list[float]],
pattern: str = "ANSI31",
angle: float = 45.0,
scale: float = 1.0,
layer: str = "HATCH",
) -> str:
"""Wrapper around backend.entity_create_hatch returning the new handle."""
info = await backend.entity_create_hatch(
pattern=pattern,
boundary_points=boundary_points,
scale=scale,
angle=angle,
layer=layer,
)
return info.handle
+1 -1
View File
@@ -55,7 +55,7 @@ class TitleBlockMetadata:
async def apply_iso_a3_titleblock(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
metadata: TitleBlockMetadata,
origin: tuple[float, float] = (0.0, 0.0),
+1 -1
View File
@@ -62,7 +62,7 @@ class DrawingValidator:
async def run(
self,
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
expected: dict | None = None,
) -> ValidationResult: