Files
cadSet/SimpleCADAPI/test/test_stdlib_fastener.py
T

360 lines
13 KiB
Python

"""Tests for parameterized bolt and nut standard parts."""
import json
import unittest
import simplecadapi as scad
class TestFastenerSurface(unittest.TestCase):
def test_preferred_nested_std_export_surface(self):
bolt = scad.std.fastener.make_bolt_rsolid(diameter=8.0, length=24.0)
nut = scad.std.fastener.make_nut_rsolid(
diameter=8.0,
width=13.0,
height=6.5,
)
self.assertIsInstance(bolt, scad.Solid)
self.assertIsInstance(nut, scad.Solid)
def test_public_factories_follow_make_rtype_naming(self):
factory_names = [
name
for name in scad.std.fastener.__all__
if callable(getattr(scad.std.fastener, name, None))
]
self.assertEqual(factory_names, ["make_bolt_rsolid", "make_nut_rsolid"])
for name in factory_names:
self.assertTrue(name.startswith("make_"), name)
self.assertIn("_r", name, name)
class TestBoltSolid(unittest.TestCase):
def test_supported_head_and_drive_styles(self):
styles = (
("hex", "none"),
("square", "slot"),
("cylindrical", "hex_socket"),
("button", "cross"),
("countersunk", "slot"),
)
for head_style, drive_style in styles:
with self.subTest(head_style=head_style, drive_style=drive_style):
bolt = scad.std.fastener.make_bolt_rsolid(
diameter=8.0,
length=24.0,
head_style=head_style,
thread_style="partial",
drive_style=drive_style,
)
meta = bolt.get_metadata("std.fastener.bolt")
self.assertGreater(bolt.get_volume(), 0.0)
self.assertEqual(meta["head_style"], head_style)
self.assertEqual(meta["drive_style"], drive_style)
self.assertEqual(meta["thread_start"], 24.0 - meta["thread_length"])
def test_thread_styles_record_expected_interval(self):
none = scad.std.fastener.make_bolt_rsolid(
diameter=8.0,
length=24.0,
thread_style="none",
)
full = scad.std.fastener.make_bolt_rsolid(
diameter=8.0,
length=24.0,
thread_style="full",
)
partial = scad.std.fastener.make_bolt_rsolid(
diameter=8.0,
length=24.0,
thread_style="partial",
thread_length=15.0,
)
none_meta = none.get_metadata("std.fastener.bolt")
full_meta = full.get_metadata("std.fastener.bolt")
partial_meta = partial.get_metadata("std.fastener.bolt")
self.assertEqual(none_meta["thread_length"], 0.0)
self.assertEqual(full_meta["thread_start"], 0.0)
self.assertEqual(full_meta["thread_length"], 24.0)
self.assertEqual(partial_meta["thread_start"], 9.0)
self.assertEqual(partial_meta["thread_length"], 15.0)
def test_metric_coarse_series_and_basic_thread_dimensions(self):
bolt = scad.std.fastener.make_bolt_rsolid(
diameter=10.0,
length=40.0,
)
meta = bolt.get_metadata("std.fastener.bolt")
self.assertEqual(meta["thread_pitch"], 1.5)
self.assertEqual(meta["thread_pitch_source"], "metric_coarse")
self.assertAlmostEqual(meta["pitch_diameter"], 10.0 - 0.6495 * 1.5, places=5)
self.assertAlmostEqual(
meta["basic_minor_diameter"],
10.0 - 1.0825 * 1.5,
places=5,
)
self.assertEqual(meta["thread_style"], "partial")
self.assertEqual(meta["thread_start"], 14.0)
self.assertEqual(meta["thread_length"], 26.0)
self.assertAlmostEqual(meta["head_width"], 16.0)
self.assertAlmostEqual(meta["head_height"], 6.4)
self.assertAlmostEqual(meta["head_across_corners"], 16.0 / 0.8660254038)
self.assertAlmostEqual(meta["underhead_fillet_radius"], 0.6)
self.assertEqual(meta["recommended_mating_hole_chamfer_min"], 0.6)
def test_explicit_thread_pitch_is_required_outside_coarse_series(self):
with self.assertRaises(ValueError):
scad.std.fastener.make_bolt_rsolid(diameter=9.0, length=24.0)
bolt = scad.std.fastener.make_bolt_rsolid(
diameter=9.0,
length=24.0,
thread_pitch=1.25,
)
self.assertEqual(
bolt.get_metadata("std.fastener.bolt")["thread_pitch_source"],
"explicit",
)
def test_auto_uses_full_thread_when_standard_partial_length_exceeds_length(self):
bolt = scad.std.fastener.make_bolt_rsolid(
diameter=4.0,
length=13.0,
thread_detail="cosmetic",
)
meta = bolt.get_metadata("std.fastener.bolt")
self.assertEqual(meta["requested_thread_style"], "auto")
self.assertEqual(meta["thread_style"], "full")
self.assertEqual(meta["thread_length"], 13.0)
def test_modeled_external_thread_strict_replay(self):
with scad.GraphSession() as session:
bolt = scad.std.fastener.make_bolt_rsolid(
diameter=8.0,
length=24.0,
head_style="button",
thread_style="partial",
thread_detail="modeled",
thread_form="v",
thread_pitch=1.25,
thread_depth=0.65,
thread_length=18.0,
drive_style="cross",
)
model_json = scad.export_model_json(session=session)
payload = json.loads(model_json)
ops = [node["op"] for node in payload["graph"]["nodes"]]
self.assertIn("make_helix_redge", ops)
self.assertIn("make_sweep_rsolid", ops)
self.assertIn("make_cut_rsolid", ops)
self.assertTrue(
all("topo_delta" not in node for node in payload["graph"]["nodes"])
)
replayed = scad.replay_model_json(json_str=model_json, strict=True)
self.assertEqual(len(replayed), 1)
self.assertAlmostEqual(replayed[0].get_volume(), bolt.get_volume(), places=5)
print(
"modeled_bolt: "
f"volume={bolt.get_volume():.3f} faces={len(bolt.get_faces())}"
)
def test_external_thread_stabilization_records_only_selected_phase(self):
with scad.GraphSession() as session:
bolt = scad.std.fastener.make_bolt_rsolid(
diameter=6.0,
length=18.0,
thread_style="full",
thread_detail="modeled",
thread_form="v",
thread_pitch=1.0,
thread_depth=0.45,
)
model_json = scad.export_model_json(session=session)
meta = bolt.get_metadata("std.fastener.bolt")
payload = json.loads(model_json)
ops = [node["op"] for node in payload["graph"]["nodes"]]
self.assertNotEqual(meta["thread_phase_degrees"], 0.0)
self.assertEqual(ops.count("make_rotate_rshape"), 1)
replayed = scad.replay_model_json(json_str=model_json, strict=True)
self.assertAlmostEqual(replayed[0].get_volume(), bolt.get_volume(), places=5)
def test_invalid_bolt_parameters(self):
invalid_kwargs = (
{"diameter": 0.0, "length": 24.0},
{"diameter": 8.0, "length": 24.0, "head_style": "wing"},
{"diameter": 8.0, "length": 24.0, "head_width": 7.0},
{
"diameter": 8.0,
"length": 24.0,
"thread_style": "partial",
"thread_length": 24.0,
},
{
"diameter": 8.0,
"length": 24.0,
"thread_style": "full",
"thread_length": 12.0,
},
{
"diameter": 8.0,
"length": 24.0,
"drive_style": "none",
"drive_size": 4.0,
},
)
for kwargs in invalid_kwargs:
with self.subTest(kwargs=kwargs), self.assertRaises(ValueError):
scad.std.fastener.make_bolt_rsolid(**kwargs)
class TestNutSolid(unittest.TestCase):
def test_supported_nut_and_hole_styles(self):
for nut_style in ("hex", "square", "round", "knurled"):
for hole_style in ("through", "blind"):
with self.subTest(nut_style=nut_style, hole_style=hole_style):
kwargs = {"hole_depth": 5.0} if hole_style == "blind" else {}
nut = scad.std.fastener.make_nut_rsolid(
diameter=8.0,
width=13.0,
height=6.5,
nut_style=nut_style,
hole_style=hole_style,
**kwargs,
)
meta = nut.get_metadata("std.fastener.nut")
self.assertGreater(nut.get_volume(), 0.0)
self.assertEqual(meta["nut_style"], nut_style)
self.assertEqual(meta["hole_style"], hole_style)
def test_blind_hole_retains_more_material_than_through_hole(self):
through = scad.std.fastener.make_nut_rsolid(
diameter=8.0,
width=13.0,
height=7.0,
hole_style="through",
)
blind = scad.std.fastener.make_nut_rsolid(
diameter=8.0,
width=13.0,
height=7.0,
hole_style="blind",
hole_depth=5.0,
)
self.assertGreater(blind.get_volume(), through.get_volume())
def test_modeled_internal_thread_strict_replay(self):
cosmetic = scad.std.fastener.make_nut_rsolid(
diameter=8.0,
width=13.0,
height=7.0,
nut_style="knurled",
hole_style="blind",
hole_depth=5.5,
thread_detail="cosmetic",
knurl_count=18,
)
with scad.GraphSession() as session:
modeled = scad.std.fastener.make_nut_rsolid(
diameter=8.0,
width=13.0,
height=7.0,
nut_style="knurled",
hole_style="blind",
hole_depth=5.5,
thread_detail="modeled",
thread_form="trapezoidal",
thread_pitch=1.25,
thread_depth=0.65,
knurl_count=18,
)
model_json = scad.export_model_json(session=session)
self.assertGreater(modeled.get_volume(), cosmetic.get_volume())
payload = json.loads(model_json)
ops = [node["op"] for node in payload["graph"]["nodes"]]
self.assertIn("make_helix_redge", ops)
self.assertIn("make_sweep_rsolid", ops)
self.assertIn("make_union_rsolid", ops)
self.assertTrue(
all("topo_delta" not in node for node in payload["graph"]["nodes"])
)
replayed = scad.replay_model_json(json_str=model_json, strict=True)
self.assertEqual(len(replayed), 1)
self.assertAlmostEqual(replayed[0].get_volume(), modeled.get_volume(), places=5)
print(
"modeled_nut: "
f"volume={modeled.get_volume():.3f} faces={len(modeled.get_faces())}"
)
def test_internal_thread_stabilization_records_only_selected_phase(self):
with scad.GraphSession() as session:
nut = scad.std.fastener.make_nut_rsolid(
diameter=6.0,
width=10.0,
height=5.0,
thread_detail="modeled",
thread_form="trapezoidal",
thread_pitch=1.0,
thread_depth=0.45,
)
model_json = scad.export_model_json(session=session)
meta = nut.get_metadata("std.fastener.nut")
payload = json.loads(model_json)
ops = [node["op"] for node in payload["graph"]["nodes"]]
self.assertNotEqual(meta["thread_phase_degrees"], 0.0)
self.assertEqual(ops.count("make_rotate_rshape"), 1)
replayed = scad.replay_model_json(json_str=model_json, strict=True)
self.assertAlmostEqual(replayed[0].get_volume(), nut.get_volume(), places=5)
def test_invalid_nut_parameters(self):
invalid_kwargs = (
{"diameter": 8.0, "width": 8.2, "height": 6.5},
{
"diameter": 8.0,
"width": 13.0,
"height": 6.5,
"nut_style": "castle",
},
{
"diameter": 8.0,
"width": 13.0,
"height": 6.5,
"hole_style": "through",
"hole_depth": 5.0,
},
{
"diameter": 8.0,
"width": 13.0,
"height": 6.5,
"hole_style": "blind",
"hole_depth": 6.5,
},
{
"diameter": 8.0,
"width": 13.0,
"height": 6.5,
"nut_style": "knurled",
"knurl_count": 6,
},
)
for kwargs in invalid_kwargs:
with self.subTest(kwargs=kwargs), self.assertRaises(ValueError):
scad.std.fastener.make_nut_rsolid(**kwargs)
if __name__ == "__main__":
unittest.main()