Adds production-grade engineering features and tools: 2D GD&T (feature control frames + datum features) via engineering/gdt.py, ISO‑129 dimension tolerance support (engineering/tolerances.py) and an ezdxf/COM plumbing for toleranced dims, a scalar drawing score + invalidity ratio (engineering/scoring.py) wired into drawing_finalize, in-place entity editors (entity_edit_text/entity_edit_geometry), and a friendly drawing_settings facade. Includes server tool wiring, COM/ezdxf backend implementations, datum-consistency critique, many unit tests, README/CLAUDE/CHANGELOG updates, and a version bump to 1.2.0. Also corrects COM backend capability label (entities_2d).
144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
"""Tests for 2D GD&T (ISO 1101) — pure layout math, backend composition, and
|
|
the datum-consistency critique focus."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from engineering.gdt import (
|
|
DIAMETER_PREFIX,
|
|
GEOMETRIC_SYMBOLS,
|
|
MATERIAL_MODIFIERS,
|
|
datum_triangle,
|
|
fcf_compartments,
|
|
fcf_layout,
|
|
)
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
# ── pure: compartments ──────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_form_tolerance_needs_no_datum():
|
|
comps = fcf_compartments("flatness", 0.1)
|
|
assert comps[0] == GEOMETRIC_SYMBOLS["flatness"]
|
|
assert comps[1] == "0.1"
|
|
assert len(comps) == 2
|
|
|
|
|
|
async def test_position_requires_datum():
|
|
with pytest.raises(ValueError):
|
|
fcf_compartments("position", 0.2) # location control, no datum
|
|
|
|
|
|
async def test_position_with_datums_and_diameter_and_modifier():
|
|
comps = fcf_compartments(
|
|
"position",
|
|
0.2,
|
|
["A", "B", "C"],
|
|
diameter=True,
|
|
modifier="M",
|
|
)
|
|
assert comps[0] == GEOMETRIC_SYMBOLS["position"]
|
|
assert comps[1] == f"{DIAMETER_PREFIX}0.2{MATERIAL_MODIFIERS['M']}"
|
|
assert comps[2:] == ["A", "B", "C"]
|
|
|
|
|
|
async def test_unknown_symbol_and_modifier_raise():
|
|
with pytest.raises(ValueError):
|
|
fcf_compartments("bogus", 0.1)
|
|
with pytest.raises(ValueError):
|
|
fcf_compartments("flatness", 0.1, modifier="Z")
|
|
|
|
|
|
# ── pure: layout geometry ───────────────────────────────────────────────────
|
|
|
|
|
|
async def test_fcf_layout_box_closed_and_dividers_count():
|
|
comps = fcf_compartments("position", 0.2, ["A", "B"], diameter=True)
|
|
lay = fcf_layout(comps, 0.0, 0.0, 5.0)
|
|
# Closed rectangle: first == last point.
|
|
assert lay["box"][0] == lay["box"][-1]
|
|
# n compartments -> n-1 internal dividers, n labels.
|
|
assert len(lay["dividers"]) == len(comps) - 1
|
|
assert len(lay["labels"]) == len(comps)
|
|
assert lay["width"] > 0 and lay["height"] == 5.0
|
|
|
|
|
|
async def test_datum_triangle_is_closed_triangle():
|
|
tri = datum_triangle(10.0, 20.0, 4.0, down=True)
|
|
assert tri[0] == (10.0, 20.0) # apex
|
|
assert tri[0] == tri[-1] # closed
|
|
assert len(tri) == 4
|
|
|
|
|
|
# ── backend composition ─────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_draw_feature_control_frame_creates_entities(backend):
|
|
await backend.drawing_apply_iso_layers("mech")
|
|
res = await backend.draw_feature_control_frame(
|
|
"perpendicularity",
|
|
0.05,
|
|
0.0,
|
|
0.0,
|
|
datums=["A"],
|
|
height=5.0,
|
|
)
|
|
assert res["ok"] is True
|
|
assert res["layer"] == "DIM"
|
|
# box + 2 dividers (3 compartments) + 3 texts = several handles
|
|
assert len(res["handles"]) >= 5
|
|
for h in res["handles"]:
|
|
assert await backend.entity_get(h) is not None
|
|
|
|
|
|
async def test_draw_datum_feature_records_letter(backend):
|
|
await backend.drawing_apply_iso_layers("mech")
|
|
res = await backend.draw_datum_feature("A", 0.0, 0.0, size=5.0)
|
|
assert res["datum"] == "A"
|
|
assert "A" in backend._gdt_datums_defined
|
|
|
|
|
|
# ── gdt critique focus ──────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_gdt_critique_flags_missing_datum(backend):
|
|
await backend.drawing_apply_iso_layers("mech")
|
|
await backend.draw_feature_control_frame(
|
|
"position",
|
|
0.2,
|
|
0.0,
|
|
0.0,
|
|
datums=["A", "B"],
|
|
diameter=True,
|
|
)
|
|
issues = await backend.drawing_critique(focus=["gdt"])
|
|
assert len(issues) == 1
|
|
assert issues[0].focus == "gdt"
|
|
assert issues[0].severity == "error"
|
|
assert set(issues[0].detail["missing_datums"]) == {"A", "B"}
|
|
|
|
|
|
async def test_gdt_critique_passes_when_datums_defined(backend):
|
|
await backend.drawing_apply_iso_layers("mech")
|
|
await backend.draw_feature_control_frame(
|
|
"position",
|
|
0.2,
|
|
0.0,
|
|
0.0,
|
|
datums=["A"],
|
|
diameter=True,
|
|
)
|
|
await backend.draw_datum_feature("A", 50.0, 0.0)
|
|
issues = await backend.drawing_critique(focus=["gdt"])
|
|
assert issues == []
|
|
|
|
|
|
async def test_clean_drawing_gdt_focus_is_noop(backend):
|
|
await backend.drawing_apply_iso_layers("mech")
|
|
# No FCFs at all — gdt must not fire, and full critique stays clean.
|
|
assert await backend.drawing_critique(focus=["gdt"]) == []
|
|
assert await backend.drawing_critique() == []
|