- ruff profile in pyproject (E/W/F/I/B/RUF); clean src/ + tests/ - remove dead code: threaded_len/s/d/coat/first_rec_for_size, self.cat in gui - 59 public re-exports added to __all__ (sorted) - renamed l->ln (E741), zip(strict=True), split long lines, [*list, x] (RUF005) - CI: quality job (ruff), test runs tests/ -k 'not gen_all' w/ coverage - README rewritten (commands, API modules, tests/lint/coverage, CI) - docs/tasks.md: P4 recorded - 136 passed, 3 deselected (regen); ruff: all checks passed
119 lines
3.8 KiB
Python
119 lines
3.8 KiB
Python
"""Validation of generated STEP models against GOST nominal dimensions.
|
|
|
|
Pattern from PartCAD / cq_warehouse: a part family is validated by asserting the
|
|
generated geometry reproduces the nominal key dimensions of the standard
|
|
(across-flats S, outer diameter D, head height k) within a relative tolerance.
|
|
|
|
Requires cadquery + OCC (STEP reading). Skipped if unavailable.
|
|
|
|
No em-dash in code: Python 3.10 source lacks UTF-8 mode (no PEP 540).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
|
|
import pytest
|
|
|
|
try:
|
|
import cadquery as cq
|
|
|
|
from fastengen.model import (
|
|
generate_bolt,
|
|
generate_nut,
|
|
generate_washer,
|
|
)
|
|
from fastengen.standard import (
|
|
BOLT_SIZES,
|
|
NUT_SIZES,
|
|
WASHER_SIZES,
|
|
get_bolt,
|
|
get_nut,
|
|
get_washer,
|
|
)
|
|
from fastengen.validate_geom import DimensionResult, validate_model
|
|
HAS_CQ = True
|
|
except ImportError:
|
|
HAS_CQ = False
|
|
|
|
pytestmark = pytest.mark.skipif(not HAS_CQ, reason="requires cadquery")
|
|
|
|
|
|
def _gen_bolt(tmp_path, size, length):
|
|
path = os.path.join(str(tmp_path), f"bolt_{size}_{length}.step")
|
|
generate_bolt(get_bolt(size, length), path)
|
|
return path
|
|
|
|
|
|
def _gen_nut(tmp_path, size):
|
|
path = os.path.join(str(tmp_path), f"nut_{size}.step")
|
|
generate_nut(get_nut(size), path)
|
|
return path
|
|
|
|
|
|
def _gen_washer(tmp_path, size):
|
|
path = os.path.join(str(tmp_path), f"washer_{size}.step")
|
|
generate_washer(get_washer(size), path)
|
|
return path
|
|
|
|
|
|
@pytest.mark.parametrize("size", list(BOLT_SIZES))
|
|
def test_bolt_across_flats_matches_standard(tmp_path, size):
|
|
"""Across-flats S of the hexagon head equals the GOST wrench size."""
|
|
path = _gen_bolt(tmp_path, size, 40)
|
|
res = validate_model(path)
|
|
assert res is not None
|
|
assert res.ok, res.failed
|
|
|
|
|
|
@pytest.mark.parametrize("size", list(NUT_SIZES))
|
|
def test_nut_across_flats_and_height(tmp_path, size):
|
|
"""Nut across-flats S and height k match the GOST table."""
|
|
path = _gen_nut(tmp_path, size)
|
|
res = validate_model(path)
|
|
assert res is not None
|
|
assert res.ok, res.failed
|
|
|
|
|
|
@pytest.mark.parametrize("size", list(WASHER_SIZES))
|
|
def test_washer_outer_and_thickness(tmp_path, size):
|
|
"""Washer outer diameter D and thickness S (k) match the GOST table."""
|
|
path = _gen_washer(tmp_path, size)
|
|
res = validate_model(path)
|
|
assert res is not None
|
|
assert res.ok, res.failed
|
|
|
|
|
|
def test_rejects_wrong_flats(tmp_path):
|
|
"""Validator must FAIL when the across-flats is outside tolerance."""
|
|
res = DimensionResult("bolt_M12_40.step", "bolt", "M12", 0.04, {})
|
|
from fastengen.validate_geom import _validate_dim
|
|
# Measured 20.0 for a standard S=18 with tol 4% (allowed 17.28..18.72).
|
|
_validate_dim(res, "across_flats_S", 18.0, 18.0 * 0.96, 18.0 * 1.04)
|
|
assert not res.ok
|
|
assert any("across_flats_S" in f for f in res.failed)
|
|
|
|
|
|
def test_dimension_result_ok_when_within_tol(tmp_path):
|
|
"""A measurement within tolerance passes."""
|
|
res = DimensionResult("nut_M12.step", "nut", "M12", 0.04, {"across_flats_S": 19.0})
|
|
from fastengen.validate_geom import _validate_dim
|
|
_validate_dim(res, "across_flats_S", 19.0, 19.0 * 0.96, 19.0 * 1.04)
|
|
assert res.ok
|
|
assert res.failed == []
|
|
|
|
|
|
def test_bbox_min_is_across_flats_for_hexagon(tmp_path):
|
|
"""Sanity: hexagon bbox min extent is across-flats (smaller than corners)."""
|
|
path = _gen_nut(tmp_path, "M12")
|
|
parts = cq.importers.importStep(path)
|
|
bb = next(iter(parts.vals())).BoundingBox()
|
|
x = bb.xmax - bb.xmin
|
|
y = bb.ymax - bb.ymin
|
|
assert min(x, y) == pytest.approx(19.0, abs=0.1) # across-flats S
|
|
# across-corners of a hexagon = 2 * across-flats / sqrt(3) ~= 1.1547*S
|
|
assert max(x, y) == pytest.approx(19.0 * 2 / (3 ** 0.5), abs=0.3)
|