style: apply ruff format across the repository
ruff format was never run repo-wide; CI now enforces 'ruff format --check', so normalize every module once. No behavior changes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
4a2e9d658f
commit
9ec6c4a989
+95
-72
@@ -29,8 +29,13 @@ _EXPECTED_LAYER_COLOR: dict[str, int] = {
|
||||
|
||||
# Dimension entity types that should sit on the DIM layer.
|
||||
_DIM_TYPES = {
|
||||
"DIMENSION", "DIMLINEAR", "DIMALIGNED", "DIMANGULAR",
|
||||
"DIMRADIUS", "DIMDIAMETER", "DIMORDINATE",
|
||||
"DIMENSION",
|
||||
"DIMLINEAR",
|
||||
"DIMALIGNED",
|
||||
"DIMANGULAR",
|
||||
"DIMRADIUS",
|
||||
"DIMDIAMETER",
|
||||
"DIMORDINATE",
|
||||
}
|
||||
|
||||
|
||||
@@ -57,15 +62,17 @@ async def _check_iso128(backend: AutoCADBackend) -> list[Issue]:
|
||||
# (e.g. 25 == 0.25mm) — they are NOT pre-normalised to mm, so accept
|
||||
# either the raw mm value or hundredths/100.
|
||||
if lw not in allowed and (lw / 100.0) not in allowed:
|
||||
issues.append(Issue(
|
||||
severity="warning",
|
||||
focus="iso128",
|
||||
message=(
|
||||
f"Layer '{lyr.name}' lineweight {lw} mm is not in the "
|
||||
f"ISO 128 set {sorted(allowed)}."
|
||||
),
|
||||
detail={"layer": lyr.name, "lineweight": lw},
|
||||
))
|
||||
issues.append(
|
||||
Issue(
|
||||
severity="warning",
|
||||
focus="iso128",
|
||||
message=(
|
||||
f"Layer '{lyr.name}' lineweight {lw} mm is not in the "
|
||||
f"ISO 128 set {sorted(allowed)}."
|
||||
),
|
||||
detail={"layer": lyr.name, "lineweight": lw},
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
@@ -79,17 +86,19 @@ async def _check_layer_color(backend: AutoCADBackend) -> list[Issue]:
|
||||
for lyr in layers:
|
||||
expected = _EXPECTED_LAYER_COLOR.get(lyr.name)
|
||||
if expected is None:
|
||||
continue # unknown layer — caller's responsibility
|
||||
continue # unknown layer — caller's responsibility
|
||||
if int(lyr.color) != int(expected):
|
||||
issues.append(Issue(
|
||||
severity="warning",
|
||||
focus="layer_color",
|
||||
message=(
|
||||
f"Layer '{lyr.name}' has color {lyr.color}; "
|
||||
f"engineering standard expects {expected}."
|
||||
),
|
||||
detail={"layer": lyr.name, "got": int(lyr.color), "expected": expected},
|
||||
))
|
||||
issues.append(
|
||||
Issue(
|
||||
severity="warning",
|
||||
focus="layer_color",
|
||||
message=(
|
||||
f"Layer '{lyr.name}' has color {lyr.color}; "
|
||||
f"engineering standard expects {expected}."
|
||||
),
|
||||
detail={"layer": lyr.name, "got": int(lyr.color), "expected": expected},
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
@@ -107,17 +116,21 @@ async def _check_construction_left(backend: AutoCADBackend) -> list[Issue]:
|
||||
leftover = [e for e in ents if "CONST" in (e.layer or "").upper()]
|
||||
if not leftover:
|
||||
return []
|
||||
return [Issue(
|
||||
severity="error",
|
||||
focus="construction_left",
|
||||
message=(
|
||||
f"{len(leftover)} entity/entities still on a construction layer. "
|
||||
"Call construction_clear() before drawing_finalize."
|
||||
),
|
||||
handles=[e.handle for e in leftover[:50]],
|
||||
detail={"count": len(leftover),
|
||||
"layers": sorted({e.layer for e in leftover if e.layer})},
|
||||
)]
|
||||
return [
|
||||
Issue(
|
||||
severity="error",
|
||||
focus="construction_left",
|
||||
message=(
|
||||
f"{len(leftover)} entity/entities still on a construction layer. "
|
||||
"Call construction_clear() before drawing_finalize."
|
||||
),
|
||||
handles=[e.handle for e in leftover[:50]],
|
||||
detail={
|
||||
"count": len(leftover),
|
||||
"layers": sorted({e.layer for e in leftover if e.layer}),
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def _check_untrimmed_corner(backend: AutoCADBackend) -> list[Issue]:
|
||||
@@ -150,16 +163,18 @@ async def _check_untrimmed_corner(backend: AutoCADBackend) -> list[Issue]:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
issues.append(Issue(
|
||||
severity="warning",
|
||||
focus="untrimmed_corner",
|
||||
message=(
|
||||
f"Lines {h1} and {h2} have endpoints {d:.3f} mm apart — "
|
||||
"likely an untrimmed corner (use entity_trim or entity_fillet)."
|
||||
),
|
||||
handles=[h1, h2],
|
||||
detail={"gap_mm": d, "p1": list(p1), "p2": list(p2)},
|
||||
))
|
||||
issues.append(
|
||||
Issue(
|
||||
severity="warning",
|
||||
focus="untrimmed_corner",
|
||||
message=(
|
||||
f"Lines {h1} and {h2} have endpoints {d:.3f} mm apart — "
|
||||
"likely an untrimmed corner (use entity_trim or entity_fillet)."
|
||||
),
|
||||
handles=[h1, h2],
|
||||
detail={"gap_mm": d, "p1": list(p1), "p2": list(p2)},
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
@@ -180,12 +195,14 @@ async def _check_duplicate_entities(backend: AutoCADBackend) -> list[Issue]:
|
||||
k2 = (k1[2], k1[3], k1[0], k1[1])
|
||||
for key in (k1, k2):
|
||||
if key in seen:
|
||||
issues.append(Issue(
|
||||
severity="warning",
|
||||
focus="duplicate_entities",
|
||||
message=f"Lines {seen[key]} and {ln.handle} are duplicates.",
|
||||
handles=[seen[key], ln.handle],
|
||||
))
|
||||
issues.append(
|
||||
Issue(
|
||||
severity="warning",
|
||||
focus="duplicate_entities",
|
||||
message=f"Lines {seen[key]} and {ln.handle} are duplicates.",
|
||||
handles=[seen[key], ln.handle],
|
||||
)
|
||||
)
|
||||
break
|
||||
else:
|
||||
seen[k1] = ln.handle
|
||||
@@ -243,12 +260,14 @@ async def _check_dim_overlap(backend: AutoCADBackend) -> list[Issue]:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
issues.append(Issue(
|
||||
severity="warning",
|
||||
focus="dim_overlap",
|
||||
message=f"Dimensions {h1} and {h2} are {d:.2f} mm apart — likely overlapping.",
|
||||
handles=[h1, h2],
|
||||
))
|
||||
issues.append(
|
||||
Issue(
|
||||
severity="warning",
|
||||
focus="dim_overlap",
|
||||
message=f"Dimensions {h1} and {h2} are {d:.2f} mm apart — likely overlapping.",
|
||||
handles=[h1, h2],
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
@@ -267,19 +286,21 @@ async def _check_gdt(backend: AutoCADBackend) -> list[Issue]:
|
||||
missing = sorted(referenced - defined)
|
||||
if not missing:
|
||||
return []
|
||||
return [Issue(
|
||||
severity="error",
|
||||
focus="gdt",
|
||||
message=(
|
||||
f"Feature control frame(s) reference datum(s) {missing} with no "
|
||||
"matching datum feature. Add draw_datum_feature for each datum."
|
||||
),
|
||||
detail={
|
||||
"missing_datums": missing,
|
||||
"defined": sorted(defined),
|
||||
"referenced": sorted(referenced),
|
||||
},
|
||||
)]
|
||||
return [
|
||||
Issue(
|
||||
severity="error",
|
||||
focus="gdt",
|
||||
message=(
|
||||
f"Feature control frame(s) reference datum(s) {missing} with no "
|
||||
"matching datum feature. Add draw_datum_feature for each datum."
|
||||
),
|
||||
detail={
|
||||
"missing_datums": missing,
|
||||
"defined": sorted(defined),
|
||||
"referenced": sorted(referenced),
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
_FOCUS_DISPATCH = {
|
||||
@@ -303,11 +324,13 @@ async def run_critique(
|
||||
for f in foci:
|
||||
check = _FOCUS_DISPATCH.get(f)
|
||||
if check is None:
|
||||
issues.append(Issue(
|
||||
severity="warning",
|
||||
focus=f, # type: ignore[arg-type]
|
||||
message=f"Unknown critique focus '{f}' (closed enum).",
|
||||
))
|
||||
issues.append(
|
||||
Issue(
|
||||
severity="warning",
|
||||
focus=f, # type: ignore[arg-type]
|
||||
message=f"Unknown critique focus '{f}' (closed enum).",
|
||||
)
|
||||
)
|
||||
continue
|
||||
issues.extend(await check(backend))
|
||||
return issues
|
||||
|
||||
@@ -163,7 +163,9 @@ async def deliver_drawing(
|
||||
expected: dict | None = None,
|
||||
) -> DeliveryResult:
|
||||
"""Export, validate, hash and re-open a release-ready drawing bundle."""
|
||||
requested = list(dict.fromkeys(item.lower().strip() for item in (formats or ["dxf", "pdf", "png"])))
|
||||
requested = list(
|
||||
dict.fromkeys(item.lower().strip() for item in (formats or ["dxf", "pdf", "png"]))
|
||||
)
|
||||
unknown = [item for item in requested if item not in _FORMAT_FILENAMES]
|
||||
if unknown:
|
||||
raise ValueError(f"Unsupported delivery format(s): {', '.join(unknown)}")
|
||||
@@ -210,9 +212,7 @@ async def deliver_drawing(
|
||||
artifact_path.write_bytes(png)
|
||||
artifacts.append(_artifact_record(fmt, artifact_path, status="created"))
|
||||
except Exception as exc:
|
||||
artifacts.append(
|
||||
_artifact_record(fmt, artifact_path, status="failed", error=str(exc))
|
||||
)
|
||||
artifacts.append(_artifact_record(fmt, artifact_path, status="failed", error=str(exc)))
|
||||
finally:
|
||||
timings[f"export_{fmt}_ms"] = round((time.perf_counter() - export_started) * 1000, 2)
|
||||
|
||||
|
||||
+35
-13
@@ -67,16 +67,18 @@ def generate_involute_flank(
|
||||
return points
|
||||
|
||||
|
||||
def _rotate_points(
|
||||
pts: list[tuple[float, float]], angle: float
|
||||
) -> list[tuple[float, float]]:
|
||||
def _rotate_points(pts: list[tuple[float, float]], angle: float) -> list[tuple[float, float]]:
|
||||
c, s = math.cos(angle), math.sin(angle)
|
||||
return [(c * x - s * y, s * x + c * y) for x, y in pts]
|
||||
|
||||
|
||||
def _arc_points(
|
||||
cx: float, cy: float, radius: float,
|
||||
start_angle: float, end_angle: float, n: int = 5,
|
||||
cx: float,
|
||||
cy: float,
|
||||
radius: float,
|
||||
start_angle: float,
|
||||
end_angle: float,
|
||||
n: int = 5,
|
||||
) -> list[tuple[float, float]]:
|
||||
"""Sample n points along an arc (start_angle → end_angle, radians, ccw)."""
|
||||
if n < 2:
|
||||
@@ -232,20 +234,29 @@ async def draw_helical_gear_front_view(
|
||||
root_circ = await backend.entity_create_circle(cx, cy, root_r, layer="GEOMETRY")
|
||||
|
||||
pts = generate_full_gear_outline(
|
||||
module=module, teeth=teeth, pressure_angle=pressure_angle,
|
||||
helix_angle=helix_angle, hand=hand, center=(cx, cy),
|
||||
module=module,
|
||||
teeth=teeth,
|
||||
pressure_angle=pressure_angle,
|
||||
helix_angle=helix_angle,
|
||||
hand=hand,
|
||||
center=(cx, cy),
|
||||
)
|
||||
outline = await backend.entity_create_polyline(
|
||||
[[float(x), float(y)] for x, y in pts],
|
||||
closed=True, layer="GEOMETRY",
|
||||
closed=True,
|
||||
layer="GEOMETRY",
|
||||
)
|
||||
|
||||
helix_handles: list[str] | None = None
|
||||
helix_label: str | None = None
|
||||
if helix_angle and abs(helix_angle) > 1e-9:
|
||||
helix_handles, helix_label = await _draw_helix_symbol(
|
||||
backend, cx=cx, cy=cy, outer_r=outer_r,
|
||||
helix_angle=helix_angle, hand=hand,
|
||||
backend,
|
||||
cx=cx,
|
||||
cy=cy,
|
||||
outer_r=outer_r,
|
||||
helix_angle=helix_angle,
|
||||
hand=hand,
|
||||
)
|
||||
|
||||
bore_handle: str | None = None
|
||||
@@ -263,7 +274,10 @@ async def draw_helical_gear_front_view(
|
||||
keyway_dict = keyed
|
||||
else:
|
||||
bore_only = await backend.entity_create_circle(
|
||||
cx, cy, bore_diameter / 2.0, layer="GEOMETRY",
|
||||
cx,
|
||||
cy,
|
||||
bore_diameter / 2.0,
|
||||
layer="GEOMETRY",
|
||||
)
|
||||
bore_handle = bore_only.handle
|
||||
|
||||
@@ -385,10 +399,18 @@ async def draw_gear_section_aa(
|
||||
bore_handles = [keyway_dict["bore_top"], keyway_dict["bore_bottom"]]
|
||||
else:
|
||||
bore_top = await backend.entity_create_line(
|
||||
x_left, cy + bore_r, x_right, cy + bore_r, layer="GEOMETRY",
|
||||
x_left,
|
||||
cy + bore_r,
|
||||
x_right,
|
||||
cy + bore_r,
|
||||
layer="GEOMETRY",
|
||||
)
|
||||
bore_bot = await backend.entity_create_line(
|
||||
x_left, cy - bore_r, x_right, cy - bore_r, layer="GEOMETRY",
|
||||
x_left,
|
||||
cy - bore_r,
|
||||
x_right,
|
||||
cy - bore_r,
|
||||
layer="GEOMETRY",
|
||||
)
|
||||
bore_handles = [bore_top.handle, bore_bot.handle]
|
||||
|
||||
|
||||
+43
-9
@@ -45,7 +45,9 @@ def keyway_dimensions(bore_diameter: float) -> dict:
|
||||
f"({DIN6885_TABLE[-1][1]} mm)."
|
||||
)
|
||||
for b_min, b_max, width, height, t1 in DIN6885_TABLE:
|
||||
if b_min < bore_diameter <= b_max or (bore_diameter == b_min and b_min == DIN6885_TABLE[0][0]):
|
||||
if b_min < bore_diameter <= b_max or (
|
||||
bore_diameter == b_min and b_min == DIN6885_TABLE[0][0]
|
||||
):
|
||||
return {
|
||||
"width": float(width),
|
||||
"height": float(height),
|
||||
@@ -95,10 +97,18 @@ async def draw_keyed_bore(
|
||||
|
||||
overshoot = max(r * 0.2, 5.0)
|
||||
horiz = await backend.entity_create_line(
|
||||
cx - r - overshoot, cy, cx + r + overshoot, cy, layer=center_layer,
|
||||
cx - r - overshoot,
|
||||
cy,
|
||||
cx + r + overshoot,
|
||||
cy,
|
||||
layer=center_layer,
|
||||
)
|
||||
vert = await backend.entity_create_line(
|
||||
cx, cy - r - overshoot, cx, cy + r + depth + overshoot, layer=center_layer,
|
||||
cx,
|
||||
cy - r - overshoot,
|
||||
cx,
|
||||
cy + r + depth + overshoot,
|
||||
layer=center_layer,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -142,23 +152,47 @@ async def draw_keyway_section(
|
||||
notch_right = cx + half_kw
|
||||
|
||||
bore_top = await backend.entity_create_line(
|
||||
x_left, top_y, x_right, top_y, layer=layer,
|
||||
x_left,
|
||||
top_y,
|
||||
x_right,
|
||||
top_y,
|
||||
layer=layer,
|
||||
)
|
||||
bore_bottom = await backend.entity_create_line(
|
||||
x_left, bot_y, x_right, bot_y, layer=layer,
|
||||
x_left,
|
||||
bot_y,
|
||||
x_right,
|
||||
bot_y,
|
||||
layer=layer,
|
||||
)
|
||||
|
||||
notch_bottom = await backend.entity_create_line(
|
||||
notch_left, top_y, notch_right, top_y, layer=layer,
|
||||
notch_left,
|
||||
top_y,
|
||||
notch_right,
|
||||
top_y,
|
||||
layer=layer,
|
||||
)
|
||||
notch_left_wall = await backend.entity_create_line(
|
||||
notch_left, top_y, notch_left, notch_top_y, layer=layer,
|
||||
notch_left,
|
||||
top_y,
|
||||
notch_left,
|
||||
notch_top_y,
|
||||
layer=layer,
|
||||
)
|
||||
notch_top = await backend.entity_create_line(
|
||||
notch_left, notch_top_y, notch_right, notch_top_y, layer=layer,
|
||||
notch_left,
|
||||
notch_top_y,
|
||||
notch_right,
|
||||
notch_top_y,
|
||||
layer=layer,
|
||||
)
|
||||
notch_right_wall = await backend.entity_create_line(
|
||||
notch_right, notch_top_y, notch_right, top_y, layer=layer,
|
||||
notch_right,
|
||||
notch_top_y,
|
||||
notch_right,
|
||||
top_y,
|
||||
layer=layer,
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
+45
-41
@@ -13,52 +13,52 @@ log = logging.getLogger(__name__)
|
||||
|
||||
ENGINEERING_LAYERS: list[tuple[str, int, str, float, str]] = [
|
||||
# (name, color, linetype, lineweight, description)
|
||||
("0", 7, "Continuous", 0.25, "default"),
|
||||
("GEOMETRY", 7, "Continuous", 0.50, "visible solid edges"),
|
||||
("HIDDEN", 3, "HIDDEN", 0.25, "hidden edges"),
|
||||
("CENTER", 1, "CENTER", 0.18, "centerlines"),
|
||||
("PHANTOM", 6, "PHANTOM", 0.18, "section / cutting planes"),
|
||||
("DIM", 2, "Continuous", 0.18, "dimensions"),
|
||||
("TEXT", 7, "Continuous", 0.25, "annotations"),
|
||||
("HATCH", 8, "Continuous", 0.13, "section hatching"),
|
||||
("TITLEBLOCK", 7, "Continuous", 0.50, "drawing border + title"),
|
||||
("0", 7, "Continuous", 0.25, "default"),
|
||||
("GEOMETRY", 7, "Continuous", 0.50, "visible solid edges"),
|
||||
("HIDDEN", 3, "HIDDEN", 0.25, "hidden edges"),
|
||||
("CENTER", 1, "CENTER", 0.18, "centerlines"),
|
||||
("PHANTOM", 6, "PHANTOM", 0.18, "section / cutting planes"),
|
||||
("DIM", 2, "Continuous", 0.18, "dimensions"),
|
||||
("TEXT", 7, "Continuous", 0.25, "annotations"),
|
||||
("HATCH", 8, "Continuous", 0.13, "section hatching"),
|
||||
("TITLEBLOCK", 7, "Continuous", 0.50, "drawing border + title"),
|
||||
("CONSTRUCTION", 250, "Continuous", 0.05, "scratch / construction"),
|
||||
]
|
||||
|
||||
|
||||
# P&ID standard layer set (de-facto, distilled from ISO 10628 + common practice).
|
||||
PID_LAYERS: list[tuple[str, int, str, float, str]] = [
|
||||
("0", 7, "Continuous", 0.25, "default"),
|
||||
("PROCESS-PIPING-MAIN", 1, "Continuous", 0.70, "main process line (heavy)"),
|
||||
("PROCESS-PIPING-SECONDARY", 2, "Continuous", 0.50, "secondary process line"),
|
||||
("PROCESS-EQUIPMENT", 7, "Continuous", 0.50, "vessels / pumps / drums"),
|
||||
("PROCESS-VALVES", 3, "Continuous", 0.35, "valves / fittings"),
|
||||
("INSTRUMENT-SYMBOL", 4, "Continuous", 0.35, "instrument bubbles / FCFs"),
|
||||
("INSTRUMENT-LINE-SIGNAL", 4, "DASHED", 0.18, "pneumatic / electrical signal"),
|
||||
("INSTRUMENT-TAG-TEXT", 7, "Continuous", 0.25, "tag labels"),
|
||||
("ELECTRICAL-LINE", 6, "DASHDOT", 0.18, "electrical line"),
|
||||
("UTILITY-LINE", 5, "PHANTOM", 0.25, "utility (steam, water, air)"),
|
||||
("INSULATION-HATCH", 8, "Continuous", 0.13, "insulation hatch"),
|
||||
("DIM", 2, "Continuous", 0.18, "dimensions"),
|
||||
("TEXT-NOTES", 7, "Continuous", 0.25, "free-form notes"),
|
||||
("TITLEBLOCK", 7, "Continuous", 0.50, "drawing border + title"),
|
||||
("CONSTRUCTION", 250, "Continuous", 0.05, "scratch / construction"),
|
||||
("0", 7, "Continuous", 0.25, "default"),
|
||||
("PROCESS-PIPING-MAIN", 1, "Continuous", 0.70, "main process line (heavy)"),
|
||||
("PROCESS-PIPING-SECONDARY", 2, "Continuous", 0.50, "secondary process line"),
|
||||
("PROCESS-EQUIPMENT", 7, "Continuous", 0.50, "vessels / pumps / drums"),
|
||||
("PROCESS-VALVES", 3, "Continuous", 0.35, "valves / fittings"),
|
||||
("INSTRUMENT-SYMBOL", 4, "Continuous", 0.35, "instrument bubbles / FCFs"),
|
||||
("INSTRUMENT-LINE-SIGNAL", 4, "DASHED", 0.18, "pneumatic / electrical signal"),
|
||||
("INSTRUMENT-TAG-TEXT", 7, "Continuous", 0.25, "tag labels"),
|
||||
("ELECTRICAL-LINE", 6, "DASHDOT", 0.18, "electrical line"),
|
||||
("UTILITY-LINE", 5, "PHANTOM", 0.25, "utility (steam, water, air)"),
|
||||
("INSULATION-HATCH", 8, "Continuous", 0.13, "insulation hatch"),
|
||||
("DIM", 2, "Continuous", 0.18, "dimensions"),
|
||||
("TEXT-NOTES", 7, "Continuous", 0.25, "free-form notes"),
|
||||
("TITLEBLOCK", 7, "Continuous", 0.50, "drawing border + title"),
|
||||
("CONSTRUCTION", 250, "Continuous", 0.05, "scratch / construction"),
|
||||
]
|
||||
|
||||
|
||||
# ISO 13567-style layer naming kit (Agent2-Element6-Presentation2-Status1).
|
||||
# This is a *starter* set — extend as needed per discipline.
|
||||
ISO13567_LAYERS: list[tuple[str, int, str, float, str]] = [
|
||||
("0", 7, "Continuous", 0.25, "default"),
|
||||
("M-GEOMET-E-N", 7, "Continuous", 0.50, "Mechanical / geometry / edge / new"),
|
||||
("M-HIDDEN-E-N", 3, "HIDDEN", 0.25, "Mechanical / hidden edge"),
|
||||
("M-CENTL-E-N", 1, "CENTER", 0.18, "Mechanical / centerlines"),
|
||||
("M-PHANT-E-N", 6, "PHANTOM", 0.18, "Mechanical / phantom"),
|
||||
("M-DIMEN-T-N", 2, "Continuous", 0.18, "Mechanical / dimension text"),
|
||||
("M-TEXT-T-N", 7, "Continuous", 0.25, "Mechanical / annotation text"),
|
||||
("M-HATCH-H-N", 8, "Continuous", 0.13, "Mechanical / hatch"),
|
||||
("M-TITLE-T-N", 7, "Continuous", 0.50, "Title block"),
|
||||
("M-CONST-E-N", 250, "Continuous", 0.05, "Construction (scratch)"),
|
||||
("0", 7, "Continuous", 0.25, "default"),
|
||||
("M-GEOMET-E-N", 7, "Continuous", 0.50, "Mechanical / geometry / edge / new"),
|
||||
("M-HIDDEN-E-N", 3, "HIDDEN", 0.25, "Mechanical / hidden edge"),
|
||||
("M-CENTL-E-N", 1, "CENTER", 0.18, "Mechanical / centerlines"),
|
||||
("M-PHANT-E-N", 6, "PHANTOM", 0.18, "Mechanical / phantom"),
|
||||
("M-DIMEN-T-N", 2, "Continuous", 0.18, "Mechanical / dimension text"),
|
||||
("M-TEXT-T-N", 7, "Continuous", 0.25, "Mechanical / annotation text"),
|
||||
("M-HATCH-H-N", 8, "Continuous", 0.13, "Mechanical / hatch"),
|
||||
("M-TITLE-T-N", 7, "Continuous", 0.50, "Title block"),
|
||||
("M-CONST-E-N", 250, "Continuous", 0.05, "Construction (scratch)"),
|
||||
]
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ LAYER_SET_REGISTRY: dict[str, list[tuple[str, int, str, float, str]]] = {
|
||||
# "DIM"/"CONSTRUCTION" (which only exist in the mech/pid sets — iso13567 uses
|
||||
# M-DIMEN-T-N / M-CONST-E-N).
|
||||
LAYER_ROLES: dict[str, dict[str, str]] = {
|
||||
"mech": {"dim": "DIM", "construction": "CONSTRUCTION"},
|
||||
"pid": {"dim": "DIM", "construction": "CONSTRUCTION"},
|
||||
"iso13567": {"dim": "M-DIMEN-T-N", "construction": "M-CONST-E-N"},
|
||||
"mech": {"dim": "DIM", "construction": "CONSTRUCTION"},
|
||||
"pid": {"dim": "DIM", "construction": "CONSTRUCTION"},
|
||||
"iso13567": {"dim": "M-DIMEN-T-N", "construction": "M-CONST-E-N"},
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ def resolve_role_layer(layer_set_id: str | None, role: str) -> str:
|
||||
roles = LAYER_ROLES.get(layer_set_id or "mech", LAYER_ROLES["mech"])
|
||||
return roles.get(role, LAYER_ROLES["mech"][role])
|
||||
|
||||
|
||||
STANDARD_LINETYPES: list[str] = ["CENTER", "HIDDEN", "PHANTOM", "DASHED", "DASHDOT"]
|
||||
|
||||
|
||||
@@ -115,14 +116,14 @@ async def ensure_engineering_layers(backend: AutoCADBackend) -> dict[str, str]:
|
||||
|
||||
|
||||
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)
|
||||
if layer_set is None:
|
||||
raise RuntimeError(
|
||||
f"Unknown layer set '{standard}'. "
|
||||
f"Choose one of: {sorted(LAYER_SET_REGISTRY)}"
|
||||
f"Unknown layer set '{standard}'. Choose one of: {sorted(LAYER_SET_REGISTRY)}"
|
||||
)
|
||||
await ensure_standard_linetypes(backend)
|
||||
|
||||
@@ -139,7 +140,10 @@ async def apply_layer_set(
|
||||
continue
|
||||
try:
|
||||
await backend.layer_create(
|
||||
name=name, color=color, linetype=linetype, lineweight=lineweight,
|
||||
name=name,
|
||||
color=color,
|
||||
linetype=linetype,
|
||||
lineweight=lineweight,
|
||||
)
|
||||
results[name] = "created"
|
||||
except Exception as exc:
|
||||
|
||||
@@ -91,5 +91,13 @@ class Issue:
|
||||
|
||||
# ISO 128 lineweight set (mm). Any lineweight outside this set is flagged.
|
||||
ISO_128_LINEWEIGHTS_MM: tuple[float, ...] = (
|
||||
0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00, 1.40, 2.00,
|
||||
0.13,
|
||||
0.18,
|
||||
0.25,
|
||||
0.35,
|
||||
0.50,
|
||||
0.70,
|
||||
1.00,
|
||||
1.40,
|
||||
2.00,
|
||||
)
|
||||
|
||||
@@ -99,9 +99,7 @@ def preflight_drawing(
|
||||
assumptions.append({"field": "units", "value": "mm", "reason": "default"})
|
||||
if allow_assumptions and not req.get("tolerance_policy"):
|
||||
req["tolerance_policy"] = "general"
|
||||
assumptions.append(
|
||||
{"field": "tolerance_policy", "value": "general", "reason": "default"}
|
||||
)
|
||||
assumptions.append({"field": "tolerance_policy", "value": "general", "reason": "default"})
|
||||
|
||||
if isinstance(req.get("units"), str):
|
||||
req["units"] = req["units"].lower()
|
||||
|
||||
@@ -64,12 +64,8 @@ async def repair_untrimmed_corner(backend: AutoCADBackend, issue: Issue) -> dict
|
||||
midpoint = ((float(p1[0]) + float(p2[0])) / 2, (float(p1[1]) + float(p2[1])) / 2)
|
||||
first = await backend.entity_get(issue.handles[0])
|
||||
second = await backend.entity_get(issue.handles[1])
|
||||
await backend.entity_edit_geometry(
|
||||
issue.handles[0], **_endpoint_kwargs(first, p1, midpoint)
|
||||
)
|
||||
await backend.entity_edit_geometry(
|
||||
issue.handles[1], **_endpoint_kwargs(second, p2, midpoint)
|
||||
)
|
||||
await backend.entity_edit_geometry(issue.handles[0], **_endpoint_kwargs(first, p1, midpoint))
|
||||
await backend.entity_edit_geometry(issue.handles[1], **_endpoint_kwargs(second, p2, midpoint))
|
||||
return {"handles": list(issue.handles), "corner": list(midpoint)}
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ async def apply_iso_a3_titleblock(
|
||||
[ox, oy + SHEET_H],
|
||||
]
|
||||
outer = await backend.entity_create_polyline(
|
||||
points=outer_pts, closed=True, layer=LAYER_BORDER,
|
||||
points=outer_pts,
|
||||
closed=True,
|
||||
layer=LAYER_BORDER,
|
||||
)
|
||||
|
||||
inner_x0 = ox + LEFT_MARGIN
|
||||
@@ -86,7 +88,9 @@ async def apply_iso_a3_titleblock(
|
||||
[inner_x0, inner_y1],
|
||||
]
|
||||
inner = await backend.entity_create_polyline(
|
||||
points=inner_pts, closed=True, layer=LAYER_BORDER,
|
||||
points=inner_pts,
|
||||
closed=True,
|
||||
layer=LAYER_BORDER,
|
||||
)
|
||||
|
||||
# ---- title block (lower-right corner of inner border) ----------------------
|
||||
@@ -103,7 +107,9 @@ async def apply_iso_a3_titleblock(
|
||||
[tb_x0, tb_y1],
|
||||
]
|
||||
tb_frame = await backend.entity_create_polyline(
|
||||
points=tb_frame_pts, closed=True, layer=LAYER_BORDER,
|
||||
points=tb_frame_pts,
|
||||
closed=True,
|
||||
layer=LAYER_BORDER,
|
||||
)
|
||||
|
||||
# Row baselines (from bottom up): row4_top, row3_top, row2_top
|
||||
@@ -115,13 +121,21 @@ async def apply_iso_a3_titleblock(
|
||||
|
||||
async def hline(y: float) -> str:
|
||||
ent = await backend.entity_create_line(
|
||||
tb_x0, y, tb_x1, y, layer=LAYER_BORDER,
|
||||
tb_x0,
|
||||
y,
|
||||
tb_x1,
|
||||
y,
|
||||
layer=LAYER_BORDER,
|
||||
)
|
||||
return ent.handle
|
||||
|
||||
async def vline(x: float, y0: float, y1: float) -> str:
|
||||
ent = await backend.entity_create_line(
|
||||
x, y0, x, y1, layer=LAYER_BORDER,
|
||||
x,
|
||||
y0,
|
||||
x,
|
||||
y1,
|
||||
layer=LAYER_BORDER,
|
||||
)
|
||||
return ent.handle
|
||||
|
||||
@@ -157,13 +171,21 @@ async def apply_iso_a3_titleblock(
|
||||
|
||||
async def label(text: str, x: float, y: float) -> str:
|
||||
ent = await backend.entity_create_text(
|
||||
text=text, x=x, y=y, height=LABEL_HEIGHT, layer=LAYER_TEXT,
|
||||
text=text,
|
||||
x=x,
|
||||
y=y,
|
||||
height=LABEL_HEIGHT,
|
||||
layer=LAYER_TEXT,
|
||||
)
|
||||
return ent.handle
|
||||
|
||||
async def value(text: str, x: float, y: float, height: float = VALUE_HEIGHT) -> str:
|
||||
ent = await backend.entity_create_text(
|
||||
text=text, x=x, y=y, height=height, layer=LAYER_TEXT,
|
||||
text=text,
|
||||
x=x,
|
||||
y=y,
|
||||
height=height,
|
||||
layer=LAYER_TEXT,
|
||||
)
|
||||
return ent.handle
|
||||
|
||||
|
||||
+131
-91
@@ -12,8 +12,13 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
REAL_DIMENSION_TYPES = {
|
||||
"DIMENSION", "DIMLINEAR", "DIMALIGNED", "DIMANGULAR",
|
||||
"DIMRADIUS", "DIMDIAMETER", "DIMORDINATE",
|
||||
"DIMENSION",
|
||||
"DIMLINEAR",
|
||||
"DIMALIGNED",
|
||||
"DIMANGULAR",
|
||||
"DIMRADIUS",
|
||||
"DIMDIAMETER",
|
||||
"DIMORDINATE",
|
||||
}
|
||||
|
||||
# Patterns that suggest a TEXT/MTEXT entity is masquerading as a dimension.
|
||||
@@ -35,7 +40,7 @@ _TITLE_LAYERS = {"TITLE", "TITLEBLOCK"}
|
||||
class ValidationFinding:
|
||||
"""One issue surfaced by the validator."""
|
||||
|
||||
severity: str # "error" | "warning" | "info"
|
||||
severity: str # "error" | "warning" | "info"
|
||||
code: str
|
||||
message: str
|
||||
detail: dict = field(default_factory=dict)
|
||||
@@ -84,40 +89,47 @@ class DrawingValidator:
|
||||
try:
|
||||
ents = await backend.entity_list(limit=10000)
|
||||
text_likes = [
|
||||
e for e in ents
|
||||
e
|
||||
for e in ents
|
||||
if e.type in ("TEXT", "MTEXT")
|
||||
and DIMENSION_LIKE_TEXT.search(
|
||||
getattr(e, "properties", {}).get("text", "") or ""
|
||||
)
|
||||
and DIMENSION_LIKE_TEXT.search(getattr(e, "properties", {}).get("text", "") or "")
|
||||
]
|
||||
real_dims = [e for e in ents if e.type in REAL_DIMENSION_TYPES]
|
||||
if text_likes:
|
||||
findings.append(ValidationFinding(
|
||||
"warning",
|
||||
"fake_dimension_text",
|
||||
f"{len(text_likes)} TEXT/MTEXT entities look like dimensions "
|
||||
"(Ø, ±, R, etc.) — use dimension_* tools instead",
|
||||
{
|
||||
"sample_handles": [t.handle for t in text_likes[:5]],
|
||||
"real_dim_count": len(real_dims),
|
||||
},
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"warning",
|
||||
"fake_dimension_text",
|
||||
f"{len(text_likes)} TEXT/MTEXT entities look like dimensions "
|
||||
"(Ø, ±, R, etc.) — use dimension_* tools instead",
|
||||
{
|
||||
"sample_handles": [t.handle for t in text_likes[:5]],
|
||||
"real_dim_count": len(real_dims),
|
||||
},
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
findings.append(ValidationFinding(
|
||||
"warning", "dim_check_failed", str(exc),
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"warning",
|
||||
"dim_check_failed",
|
||||
str(exc),
|
||||
)
|
||||
)
|
||||
|
||||
# 2 — hidden orphans
|
||||
try:
|
||||
hidden = [e for e in ents if not e.visible]
|
||||
if hidden:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"hidden_orphans",
|
||||
f"{len(hidden)} entities have visible=False — hide-fallback "
|
||||
"is forbidden, use entity_delete",
|
||||
{"sample_handles": [h.handle for h in hidden[:5]]},
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"error",
|
||||
"hidden_orphans",
|
||||
f"{len(hidden)} entities have visible=False — hide-fallback "
|
||||
"is forbidden, use entity_delete",
|
||||
{"sample_handles": [h.handle for h in hidden[:5]]},
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -126,45 +138,53 @@ class DrawingValidator:
|
||||
lts = {x.lower() for x in await backend.linetype_list()}
|
||||
missing = [lt for lt in ("center", "hidden", "phantom") if lt not in lts]
|
||||
if missing:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"linetypes_missing",
|
||||
f"Standard linetypes not loaded: {', '.join(missing).upper()}",
|
||||
{"missing": missing},
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"error",
|
||||
"linetypes_missing",
|
||||
f"Standard linetypes not loaded: {', '.join(missing).upper()}",
|
||||
{"missing": missing},
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
findings.append(ValidationFinding(
|
||||
"warning", "linetype_check_failed", str(exc),
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"warning",
|
||||
"linetype_check_failed",
|
||||
str(exc),
|
||||
)
|
||||
)
|
||||
|
||||
# 4 — gear views (heuristic, only if expected demands)
|
||||
if expected.get("must_have_bore") or expected.get("must_have_keyway"):
|
||||
bore_circles = [
|
||||
e for e in ents
|
||||
if e.type == "CIRCLE" and e.layer == "GEOMETRY"
|
||||
]
|
||||
bore_circles = [e for e in ents if e.type == "CIRCLE" and e.layer == "GEOMETRY"]
|
||||
if expected.get("must_have_bore") and len(bore_circles) < 1:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"bore_missing",
|
||||
"Expected bore but no circle on GEOMETRY layer found",
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"error",
|
||||
"bore_missing",
|
||||
"Expected bore but no circle on GEOMETRY layer found",
|
||||
)
|
||||
)
|
||||
# draw_keyed_bore emits the keyway notch as an OPEN 4-point polyline
|
||||
# on GEOMETRY — requiring `closed` here made this a permanent
|
||||
# false-negative. Match a small (3-6 point) polyline, open or closed.
|
||||
keyway_polys = [
|
||||
e for e in ents
|
||||
e
|
||||
for e in ents
|
||||
if e.type in ("LWPOLYLINE", "POLYLINE")
|
||||
and e.layer == "GEOMETRY"
|
||||
and 3 <= len(e.properties.get("points", [])) <= 6
|
||||
]
|
||||
if expected.get("must_have_keyway") and len(keyway_polys) < 1:
|
||||
findings.append(ValidationFinding(
|
||||
"warning",
|
||||
"keyway_section_unverified",
|
||||
"Could not confirm keyway via heuristic "
|
||||
"(small 3-6 point polyline on GEOMETRY)",
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"warning",
|
||||
"keyway_section_unverified",
|
||||
"Could not confirm keyway via heuristic "
|
||||
"(small 3-6 point polyline on GEOMETRY)",
|
||||
)
|
||||
)
|
||||
|
||||
# 5 — title consistency
|
||||
helix = expected.get("helix_angle")
|
||||
@@ -190,19 +210,23 @@ class DrawingValidator:
|
||||
has_spur = bool(_SPUR_RE.search(joined))
|
||||
has_helical = bool(_HELICAL_RE.search(joined))
|
||||
if has_spur and has_helical:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"title_helical_spur",
|
||||
"Title contains 'HELICAL SPUR GEAR' — invalid; use "
|
||||
"'HELICAL GEAR' or 'SPUR GEAR'",
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"error",
|
||||
"title_helical_spur",
|
||||
"Title contains 'HELICAL SPUR GEAR' — invalid; use "
|
||||
"'HELICAL GEAR' or 'SPUR GEAR'",
|
||||
)
|
||||
)
|
||||
elif has_spur:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"title_says_spur_for_helical",
|
||||
f"Title contains SPUR but part is helical (helix_angle={helix})",
|
||||
{"sample": joined[:200]},
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"error",
|
||||
"title_says_spur_for_helical",
|
||||
f"Title contains SPUR but part is helical (helix_angle={helix})",
|
||||
{"sample": joined[:200]},
|
||||
)
|
||||
)
|
||||
|
||||
# 6 — saved to disk
|
||||
try:
|
||||
@@ -210,46 +234,62 @@ class DrawingValidator:
|
||||
saved_attr = getattr(info, "saved", None)
|
||||
full_path = getattr(info, "full_path", "") or ""
|
||||
if not saved_attr:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"not_saved",
|
||||
"Drawing has unsaved changes — call drawing_save before finalize",
|
||||
{"full_path": full_path},
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"error",
|
||||
"not_saved",
|
||||
"Drawing has unsaved changes — call drawing_save before finalize",
|
||||
{"full_path": full_path},
|
||||
)
|
||||
)
|
||||
elif full_path and not Path(full_path).exists():
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"file_missing",
|
||||
f"Drawing reports saved=True but file not on disk: {full_path}",
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"error",
|
||||
"file_missing",
|
||||
f"Drawing reports saved=True but file not on disk: {full_path}",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
findings.append(ValidationFinding(
|
||||
"warning", "save_check_failed", str(exc),
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"warning",
|
||||
"save_check_failed",
|
||||
str(exc),
|
||||
)
|
||||
)
|
||||
|
||||
# 7 — screenshot
|
||||
try:
|
||||
shot = await backend.view_screenshot()
|
||||
if not shot:
|
||||
findings.append(ValidationFinding(
|
||||
"info",
|
||||
"screenshot_unavailable",
|
||||
"Screenshot returned no bytes (ezdxf needs matplotlib install)",
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"info",
|
||||
"screenshot_unavailable",
|
||||
"Screenshot returned no bytes (ezdxf needs matplotlib install)",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
findings.append(ValidationFinding(
|
||||
"info", "screenshot_failed", str(exc),
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"info",
|
||||
"screenshot_failed",
|
||||
str(exc),
|
||||
)
|
||||
)
|
||||
|
||||
# 8 — dwg path returned
|
||||
try:
|
||||
info = await backend.drawing_info()
|
||||
if not getattr(info, "full_path", ""):
|
||||
findings.append(ValidationFinding(
|
||||
"warning",
|
||||
"no_path",
|
||||
"drawing_info.full_path is empty",
|
||||
))
|
||||
findings.append(
|
||||
ValidationFinding(
|
||||
"warning",
|
||||
"no_path",
|
||||
"drawing_info.full_path is empty",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user