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.
246 lines
11 KiB
Python
246 lines
11 KiB
Python
"""Tests for the premium meta-tool layer (Task #10).
|
|
|
|
Coverage: drawing_plan, drawing_critique, point_from_snap, construction_*,
|
|
drawing_apply_iso_layers, dimension_auto, entity_select_smart.
|
|
Plus a small integration pipeline: plan → draw → critique.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
def _close(a, b, tol=1e-6):
|
|
return abs(a - b) < tol
|
|
|
|
|
|
# ─── drawing_plan ──────────────────────────────────────────────────────────
|
|
|
|
class TestDrawingPlan:
|
|
async def test_plan_returns_spec_with_intent(self, backend):
|
|
plan = await backend.drawing_plan(
|
|
"L-bracket 50x80", sheet_size="A4", scale=1.0,
|
|
)
|
|
assert plan.intent == "L-bracket 50x80"
|
|
assert plan.sheet_size == "A4"
|
|
assert plan.scale == 1.0
|
|
assert plan.layer_set_id == "mech"
|
|
|
|
async def test_plan_to_dict(self, backend):
|
|
plan = await backend.drawing_plan("X", sheet_size="A3", scale=2.0)
|
|
d = plan.to_dict()
|
|
assert d["intent"] == "X"
|
|
assert d["sheet_size"] == "A3"
|
|
assert d["notes"] == []
|
|
|
|
|
|
# ─── drawing_critique ──────────────────────────────────────────────────────
|
|
|
|
class TestDrawingCritique:
|
|
async def test_critique_clean_drawing(self, backend):
|
|
# Bootstrap layer set so layer_color/iso128 don't trigger.
|
|
await backend.drawing_apply_iso_layers("mech")
|
|
issues = await backend.drawing_critique()
|
|
assert issues == []
|
|
|
|
async def test_critique_construction_left(self, backend):
|
|
await backend.construction_xline(0, 0, 0)
|
|
issues = await backend.drawing_critique(focus=["construction_left"])
|
|
assert len(issues) == 1
|
|
assert issues[0].focus == "construction_left"
|
|
assert issues[0].severity == "error"
|
|
|
|
async def test_critique_construction_clear_resets(self, backend):
|
|
await backend.construction_xline(0, 0, 0)
|
|
await backend.construction_clear()
|
|
issues = await backend.drawing_critique(focus=["construction_left"])
|
|
assert issues == []
|
|
|
|
async def test_critique_untrimmed_corner(self, backend):
|
|
# Two lines with a 0.1 mm gap — should flag.
|
|
await backend.entity_create_line(0, 0, 50, 0)
|
|
await backend.entity_create_line(50.1, 0, 50.1, 50)
|
|
issues = await backend.drawing_critique(focus=["untrimmed_corner"])
|
|
assert len(issues) >= 1
|
|
assert all(i.focus == "untrimmed_corner" for i in issues)
|
|
|
|
async def test_critique_duplicate_entities(self, backend):
|
|
await backend.entity_create_line(0, 0, 100, 0)
|
|
await backend.entity_create_line(0, 0, 100, 0)
|
|
issues = await backend.drawing_critique(focus=["duplicate_entities"])
|
|
assert len(issues) >= 1
|
|
|
|
|
|
# ─── point_from_snap ───────────────────────────────────────────────────────
|
|
|
|
class TestPointFromSnap:
|
|
async def test_snap_end_default(self, backend):
|
|
line = await backend.entity_create_line(0, 0, 100, 0)
|
|
pt = await backend.point_from_snap(line.handle, "end")
|
|
assert pt == (0.0, 0.0)
|
|
|
|
async def test_snap_end_with_ref(self, backend):
|
|
line = await backend.entity_create_line(0, 0, 100, 0)
|
|
pt = await backend.point_from_snap(line.handle, "end", ref_x=90, ref_y=0)
|
|
assert pt == (100.0, 0.0)
|
|
|
|
async def test_snap_mid(self, backend):
|
|
line = await backend.entity_create_line(0, 0, 100, 0)
|
|
pt = await backend.point_from_snap(line.handle, "mid")
|
|
assert pt == (50.0, 0.0)
|
|
|
|
async def test_snap_perp(self, backend):
|
|
line = await backend.entity_create_line(0, 0, 100, 0)
|
|
pt = await backend.point_from_snap(line.handle, "perp", ref_x=50, ref_y=30)
|
|
assert pt == (50.0, 0.0)
|
|
|
|
async def test_snap_near_clamped(self, backend):
|
|
line = await backend.entity_create_line(0, 0, 100, 0)
|
|
pt = await backend.point_from_snap(line.handle, "near", ref_x=200, ref_y=30)
|
|
assert pt == (100.0, 0.0)
|
|
|
|
async def test_snap_circle_center(self, backend):
|
|
c = await backend.entity_create_circle(50, 50, 10)
|
|
pt = await backend.point_from_snap(c.handle, "center")
|
|
assert pt == (50.0, 50.0)
|
|
|
|
async def test_snap_circle_quad_east(self, backend):
|
|
c = await backend.entity_create_circle(50, 50, 10)
|
|
pt = await backend.point_from_snap(c.handle, "quad", ref_x=70, ref_y=50)
|
|
assert pt == (60.0, 50.0)
|
|
|
|
async def test_snap_unknown_raises(self, backend):
|
|
line = await backend.entity_create_line(0, 0, 100, 0)
|
|
with pytest.raises(RuntimeError, match="unknown snap"):
|
|
await backend.point_from_snap(line.handle, "moonbeam")
|
|
|
|
async def test_snap_perp_requires_ref(self, backend):
|
|
line = await backend.entity_create_line(0, 0, 100, 0)
|
|
with pytest.raises(RuntimeError, match="ref_x"):
|
|
await backend.point_from_snap(line.handle, "perp")
|
|
|
|
|
|
# ─── construction layer ───────────────────────────────────────────────────
|
|
|
|
class TestConstructionLayer:
|
|
async def test_xline_creates_on_construction_layer(self, backend):
|
|
xl = await backend.construction_xline(0, 0, 45)
|
|
assert xl.layer == "CONSTRUCTION"
|
|
|
|
async def test_clear_removes_xlines(self, backend):
|
|
await backend.construction_xline(0, 0, 0)
|
|
await backend.construction_xline(10, 10, 90)
|
|
result = await backend.construction_clear()
|
|
assert result["deleted"] == 2
|
|
|
|
async def test_clear_empty_layer_is_zero(self, backend):
|
|
result = await backend.construction_clear()
|
|
assert result["deleted"] == 0
|
|
|
|
|
|
# ─── drawing_apply_iso_layers ──────────────────────────────────────────────
|
|
|
|
class TestApplyISOLayers:
|
|
async def test_mech_layer_set(self, backend):
|
|
result = await backend.drawing_apply_iso_layers("mech")
|
|
assert result["ok"] is True
|
|
layers = await backend.layer_list()
|
|
names = {lyr.name for lyr in layers}
|
|
assert {"GEOMETRY", "DIM", "CENTER", "HIDDEN", "CONSTRUCTION"}.issubset(names)
|
|
|
|
async def test_pid_layer_set(self, backend):
|
|
result = await backend.drawing_apply_iso_layers("pid")
|
|
assert result["ok"] is True
|
|
layers = await backend.layer_list()
|
|
names = {lyr.name for lyr in layers}
|
|
assert "PROCESS-PIPING-MAIN" in names
|
|
assert "INSTRUMENT-SYMBOL" in names
|
|
|
|
async def test_iso13567_layer_set(self, backend):
|
|
result = await backend.drawing_apply_iso_layers("iso13567")
|
|
assert result["ok"] is True
|
|
layers = await backend.layer_list()
|
|
names = {lyr.name for lyr in layers}
|
|
assert "M-GEOMET-E-N" in names
|
|
|
|
async def test_bad_standard_raises(self, backend):
|
|
with pytest.raises(RuntimeError, match="Unknown layer set"):
|
|
await backend.drawing_apply_iso_layers("mars")
|
|
|
|
|
|
# ─── dimension_auto ────────────────────────────────────────────────────────
|
|
|
|
class TestDimensionAuto:
|
|
async def test_chain_creates_one_dim_per_segment(self, backend):
|
|
l1 = await backend.entity_create_line(0, 0, 30, 0)
|
|
l2 = await backend.entity_create_line(30, 0, 70, 0)
|
|
l3 = await backend.entity_create_line(70, 0, 100, 0)
|
|
dims = await backend.dimension_auto(
|
|
[l1.handle, l2.handle, l3.handle], style="chain",
|
|
)
|
|
assert len(dims) == 3
|
|
|
|
async def test_baseline_creates_dims(self, backend):
|
|
l1 = await backend.entity_create_line(0, 0, 30, 0)
|
|
l2 = await backend.entity_create_line(30, 0, 70, 0)
|
|
dims = await backend.dimension_auto([l1.handle, l2.handle], style="baseline")
|
|
assert len(dims) == 2
|
|
|
|
async def test_unknown_style_raises(self, backend):
|
|
ln = await backend.entity_create_line(0, 0, 50, 0)
|
|
with pytest.raises(RuntimeError, match="unknown style"):
|
|
await backend.dimension_auto([ln.handle], style="diagonal")
|
|
|
|
async def test_empty_handles_returns_empty(self, backend):
|
|
result = await backend.dimension_auto([], style="chain")
|
|
assert result == []
|
|
|
|
|
|
# ─── entity_select_smart ───────────────────────────────────────────────────
|
|
|
|
class TestEntitySelectSmart:
|
|
async def test_select_by_type(self, backend):
|
|
await backend.entity_create_line(0, 0, 50, 0)
|
|
await backend.entity_create_line(0, 50, 50, 50)
|
|
await backend.entity_create_circle(25, 25, 5)
|
|
result = await backend.entity_select_smart({"type": "LINE"})
|
|
assert len(result) == 2
|
|
|
|
async def test_select_by_layer(self, backend):
|
|
await backend.layer_create("ALPHA", color=2)
|
|
await backend.entity_create_line(0, 0, 50, 0, layer="ALPHA")
|
|
await backend.entity_create_line(0, 50, 50, 50) # default layer
|
|
result = await backend.entity_select_smart({"layer": "ALPHA"})
|
|
assert len(result) == 1
|
|
|
|
async def test_select_by_near(self, backend):
|
|
await backend.entity_create_line(0, 0, 10, 0) # near origin
|
|
await backend.entity_create_line(1000, 1000, 1010, 1000) # far
|
|
result = await backend.entity_select_smart({"type": "LINE", "near": [0, 0, 50]})
|
|
assert len(result) == 1
|
|
|
|
async def test_select_by_length_range(self, backend):
|
|
await backend.entity_create_line(0, 0, 10, 0) # length 10
|
|
await backend.entity_create_line(0, 50, 100, 50) # length 100
|
|
result = await backend.entity_select_smart({"type": "LINE", "length_range": [50, 200]})
|
|
assert len(result) == 1
|
|
|
|
|
|
# ─── Integration: plan → draw → critique pipeline ──────────────────────────
|
|
|
|
class TestPremiumPipeline:
|
|
async def test_full_l_bracket_pipeline_clean(self, backend):
|
|
"""plan → apply_layers → draw + fillet → clear construction → critique = []."""
|
|
plan = await backend.drawing_plan("L-bracket", sheet_size="A4", scale=1.0)
|
|
assert plan.intent == "L-bracket"
|
|
await backend.drawing_apply_iso_layers("mech")
|
|
l1 = await backend.entity_create_line(0, 0, 100, 0, layer="GEOMETRY")
|
|
l2 = await backend.entity_create_line(100, 0, 100, 80, layer="GEOMETRY")
|
|
await backend.entity_fillet(l1.handle, l2.handle, radius=5)
|
|
await backend.construction_clear()
|
|
# Closed corner with fillet, no construction left, no duplicates.
|
|
issues = await backend.drawing_critique(
|
|
focus=["construction_left", "untrimmed_corner", "duplicate_entities"],
|
|
)
|
|
assert issues == []
|