From 53c143e0b751b2af3907547ad6a05f6d9cab8e00 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 7 Jul 2024 21:45:10 -0700 Subject: [PATCH] test: Check assembly collision --- nhf/build.py | 62 ++++++++++++++++++++++++++++- nhf/checks.py | 7 ++++ nhf/parts/handle.py | 4 ++ nhf/parts/test.py | 8 ++++ nhf/touhou/houjuu_nue/__init__.py | 66 ++++++++++++++++--------------- nhf/touhou/houjuu_nue/test.py | 14 ++----- 6 files changed, 118 insertions(+), 43 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 4435508..25e957f 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -21,6 +21,7 @@ from typing import Union from functools import wraps from colorama import Fore, Style import cadquery as Cq +import nhf.checks as NC class TargetKind(Enum): @@ -31,6 +32,9 @@ class TargetKind(Enum): self.ext = ext class Target: + """ + Marks a function's output for serialization + """ def __init__(self, method, @@ -99,6 +103,59 @@ def target(name, **deco_kwargs): return wrapper return f +class Assembly: + """ + Marks a function's output for assembly property checking + """ + + def __init__(self, + method, + collision_check: bool = True, + **kwargs): + self._method = method + self.name = method.__name__ + self.collision_check = collision_check + self.kwargs = kwargs + def __str__(self): + return f"" + def __call__(self, obj, *args, **kwargs): + """ + Raw call function which passes arguments directly to `_method` + """ + return self._method(obj, *args, **kwargs) + + def check(self, obj): + x = self._method(obj) + assert isinstance(x, Cq.Assembly) + if self.collision_check: + intersections = NC.pairwise_intersection(x) + assert not intersections, f"In {self}, collision detected: {intersections}" + + @classmethod + def methods(cls, subject): + """ + List of all methods of a class or objects annotated with this decorator. + """ + def g(): + for name in dir(subject): + if name == 'target_names': + continue + method = getattr(subject, name) + if hasattr(method, '_assembly'): + yield method._assembly + return {method.name: method for method in g()} + +def assembly(**deco_kwargs): + """ + Decorator for annotating an assembly output + """ + def f(method): + @wraps(method) + def wrapper(self, *args, **kwargs): + return method(self, *args, **kwargs) + wrapper._assembly = Assembly(method, **deco_kwargs) + return wrapper + return f class Model: """ @@ -116,12 +173,15 @@ class Model: def check_all(self) -> int: """ - Builds all targets but do not output them + Build all models and run all the checks. Return number of checks passed """ total = 0 for t in Target.methods(self).values(): t(self) total += 1 + for t in Assembly.methods(self).values(): + t.check(self) + total += 1 return total def build_all(self, output_dir: Union[Path, str] = "build", verbose=1): diff --git a/nhf/checks.py b/nhf/checks.py index 19914ab..a5a413d 100644 --- a/nhf/checks.py +++ b/nhf/checks.py @@ -24,6 +24,13 @@ def pairwise_intersection(assembly: Cq.Assembly, tol: float=1e-6) -> list[(str, if i2 <= i1: # Remove the upper diagonal continue + head = name.split('/', 2)[1] + head2 = name2.split('/', 2)[1] + if head == head2: + # Do not test into subassemblies + continue + + vol = sh1.intersect(sh2).Volume() if vol > tol: result.append((name, name2, vol)) diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index c6845ef..bdeb459 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -69,6 +69,8 @@ class Handle: .cylinder( radius=self.diam / 2, height=length) + .faces(">Z") + .hole(self.diam_inner) ) result.faces("Z").tag("mate2") @@ -200,6 +202,8 @@ class Handle: result .union( thread + # Avoids collision in some mating cases + .rotate((0,0,0), (1,0,0), angleDegrees=180) .located(Cq.Location((0, 0, height)))) ) return result diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 93ab7c1..4445d57 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -56,16 +56,24 @@ class TestJoints(unittest.TestCase): class TestHandle(unittest.TestCase): + def test_handle_collision(self): + h = handle.Handle() + assembly = h.connector_insertion_assembly() + self.assertEqual(pairwise_intersection(assembly), []) def test_handle_assembly(self): h = handle.Handle() assembly = h.connector_insertion_assembly() bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam) + + def test_one_sided_insertion(self): + h = handle.Handle() assembly = h.connector_one_side_insertion_assembly() bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam) + self.assertEqual(pairwise_intersection(assembly), []) class TestMetricThreads(unittest.TestCase): diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index db534ec..564a7fc 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -33,7 +33,7 @@ from dataclasses import dataclass, field import unittest import cadquery as Cq from nhf import Material, Role -from nhf.build import Model, TargetKind, target +from nhf.build import Model, TargetKind, target, assembly from nhf.parts.joints import HirthJoint, TorsionJoint from nhf.parts.handle import Handle import nhf.touhou.houjuu_nue.wing as MW @@ -265,6 +265,26 @@ class Parameters(Model): result.faces(" Cq.Assembly: + harness = self.harness() + result = ( + Cq.Assembly() + .add(harness, name="base", color=Material.WOOD_BIRCH.color) + .constrain("base", "Fixed") + ) + for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: + j = self.hs_joint_parent() + ( + result + .add(j, name=name, color=Role.PARENT.color) + .constrain("base?mount", f"{name}?base", "Axis") + ) + for i in range(4): + result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") + result.solve() + return result + #@target(name="wing/joining-plate", kind=TargetKind.DXF) #def joining_plate(self) -> Cq.Workplane: # return self.wing_joining_plate.plate() @@ -289,11 +309,11 @@ class Parameters(Model): result = ( self.shoulder_torsion_joint.rider() .copyWorkplane(Cq.Workplane( - 'YZ', origin=Cq.Vector((90, 0, self.wing_root_wall_thickness)))) + 'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness)))) .rect(25, 7, centered=(True, False)) .extrude("next") .copyWorkplane(Cq.Workplane( - 'YX', origin=Cq.Vector((55, 0, self.wing_root_wall_thickness)))) + 'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness)))) .hole(self.shoulder_attach_diam) .moveTo(0, self.shoulder_attach_dist) .hole(self.shoulder_attach_diam) @@ -355,38 +375,11 @@ class Parameters(Model): ) plane = result.faces(">Z" if front else " Cq.Assembly: - return MT.trident_assembly(self.trident_handle) - - def harness_assembly(self) -> Cq.Assembly: - harness = self.harness() - result = ( - Cq.Assembly() - .add(harness, name="base", color=Material.WOOD_BIRCH.color) - .constrain("base", "Fixed") - ) - for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: - j = self.hs_joint_parent() - ( - result - .add(j, name=name, color=Role.PARENT.color) - .constrain("base?mount", f"{name}?base", "Axis") - ) - for i in range(4): - result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") - result.solve() return result + @assembly() def shoulder_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() @@ -415,6 +408,7 @@ class Parameters(Model): ) return result + @assembly() def wing_r1s1_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() @@ -438,6 +432,7 @@ class Parameters(Model): return result + @assembly() def wing_r1_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() @@ -452,6 +447,7 @@ class Parameters(Model): ) return result + @assembly() def wings_assembly(self) -> Cq.Assembly: """ Assembly of harness with all the wings @@ -474,6 +470,14 @@ class Parameters(Model): ) return result + @assembly(collision_check=False) + def trident_assembly(self) -> Cq.Assembly: + """ + Disable collision check since the threads may not align. + """ + return MT.trident_assembly(self.trident_handle) + + if __name__ == '__main__': p = Parameters() diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 512b95b..42bd607 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -9,11 +9,6 @@ class Test(unittest.TestCase): p = M.Parameters() obj = p.hs_joint_parent() self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") - def test_shoulder_joint(self): - p = M.Parameters() - shoulder = p.shoulder_assembly() - assert isinstance(shoulder, Cq.Assembly) - self.assertEqual(pairwise_intersection(shoulder), []) def test_wing_root(self): p = M.Parameters() @@ -26,12 +21,6 @@ class Test(unittest.TestCase): self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.zlen, 255, msg=msg) - def test_wings_assembly(self): - p = M.Parameters() - p.wings_assembly() - def test_harness_assembly(self): - p = M.Parameters() - p.harness_assembly() def test_trident_assembly(self): p = M.Parameters() assembly = p.trident_assembly() @@ -39,6 +28,9 @@ class Test(unittest.TestCase): length = bbox.zlen self.assertGreater(length, 1300) self.assertLess(length, 1700) + #def test_assemblies(self): + # p = M.Parameters() + # p.check_all() if __name__ == '__main__': unittest.main()