test: Check assembly collision

This commit is contained in:
Leni Aniva 2024-07-07 21:45:10 -07:00
parent d43482f77d
commit 53c143e0b7
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
6 changed files with 118 additions and 43 deletions

View File

@ -21,6 +21,7 @@ from typing import Union
from functools import wraps from functools import wraps
from colorama import Fore, Style from colorama import Fore, Style
import cadquery as Cq import cadquery as Cq
import nhf.checks as NC
class TargetKind(Enum): class TargetKind(Enum):
@ -31,6 +32,9 @@ class TargetKind(Enum):
self.ext = ext self.ext = ext
class Target: class Target:
"""
Marks a function's output for serialization
"""
def __init__(self, def __init__(self,
method, method,
@ -99,6 +103,59 @@ def target(name, **deco_kwargs):
return wrapper return wrapper
return f 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"<assembly {self.name} {self._method}>"
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: class Model:
""" """
@ -116,12 +173,15 @@ class Model:
def check_all(self) -> int: 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 total = 0
for t in Target.methods(self).values(): for t in Target.methods(self).values():
t(self) t(self)
total += 1 total += 1
for t in Assembly.methods(self).values():
t.check(self)
total += 1
return total return total
def build_all(self, output_dir: Union[Path, str] = "build", verbose=1): def build_all(self, output_dir: Union[Path, str] = "build", verbose=1):

View File

@ -24,6 +24,13 @@ def pairwise_intersection(assembly: Cq.Assembly, tol: float=1e-6) -> list[(str,
if i2 <= i1: if i2 <= i1:
# Remove the upper diagonal # Remove the upper diagonal
continue 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() vol = sh1.intersect(sh2).Volume()
if vol > tol: if vol > tol:
result.append((name, name2, vol)) result.append((name, name2, vol))

View File

@ -69,6 +69,8 @@ class Handle:
.cylinder( .cylinder(
radius=self.diam / 2, radius=self.diam / 2,
height=length) height=length)
.faces(">Z")
.hole(self.diam_inner)
) )
result.faces("<Z").tag("mate1") result.faces("<Z").tag("mate1")
result.faces(">Z").tag("mate2") result.faces(">Z").tag("mate2")
@ -200,6 +202,8 @@ class Handle:
result result
.union( .union(
thread thread
# Avoids collision in some mating cases
.rotate((0,0,0), (1,0,0), angleDegrees=180)
.located(Cq.Location((0, 0, height)))) .located(Cq.Location((0, 0, height))))
) )
return result return result

View File

@ -56,16 +56,24 @@ class TestJoints(unittest.TestCase):
class TestHandle(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): def test_handle_assembly(self):
h = handle.Handle() h = handle.Handle()
assembly = h.connector_insertion_assembly() assembly = h.connector_insertion_assembly()
bbox = assembly.toCompound().BoundingBox() bbox = assembly.toCompound().BoundingBox()
self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.xlen, h.diam)
self.assertAlmostEqual(bbox.ylen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam)
def test_one_sided_insertion(self):
h = handle.Handle()
assembly = h.connector_one_side_insertion_assembly() assembly = h.connector_one_side_insertion_assembly()
bbox = assembly.toCompound().BoundingBox() bbox = assembly.toCompound().BoundingBox()
self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.xlen, h.diam)
self.assertAlmostEqual(bbox.ylen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam)
self.assertEqual(pairwise_intersection(assembly), [])
class TestMetricThreads(unittest.TestCase): class TestMetricThreads(unittest.TestCase):

View File

@ -33,7 +33,7 @@ from dataclasses import dataclass, field
import unittest import unittest
import cadquery as Cq import cadquery as Cq
from nhf import Material, Role 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.joints import HirthJoint, TorsionJoint
from nhf.parts.handle import Handle from nhf.parts.handle import Handle
import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.wing as MW
@ -265,6 +265,26 @@ class Parameters(Model):
result.faces("<Z").tag("base") result.faces("<Z").tag("base")
return result return result
@assembly()
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
#@target(name="wing/joining-plate", kind=TargetKind.DXF) #@target(name="wing/joining-plate", kind=TargetKind.DXF)
#def joining_plate(self) -> Cq.Workplane: #def joining_plate(self) -> Cq.Workplane:
# return self.wing_joining_plate.plate() # return self.wing_joining_plate.plate()
@ -289,11 +309,11 @@ class Parameters(Model):
result = ( result = (
self.shoulder_torsion_joint.rider() self.shoulder_torsion_joint.rider()
.copyWorkplane(Cq.Workplane( .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)) .rect(25, 7, centered=(True, False))
.extrude("next") .extrude("next")
.copyWorkplane(Cq.Workplane( .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) .hole(self.shoulder_attach_diam)
.moveTo(0, self.shoulder_attach_dist) .moveTo(0, self.shoulder_attach_dist)
.hole(self.shoulder_attach_diam) .hole(self.shoulder_attach_diam)
@ -355,38 +375,11 @@ class Parameters(Model):
) )
plane = result.faces(">Z" if front else "<Z").workplane() plane = result.faces(">Z" if front else "<Z").workplane()
sign = 1 if front else -1 sign = 1 if front else -1
tag_direction = "+Y" if front else "-Y"
for name, px, py in anchors: for name, px, py in anchors:
plane.moveTo(px, sign * py).tagPlane(name) plane.moveTo(px, sign * py).tagPlane(name)
#plane.moveTo(px, sign * py).tagPlane(f"{name}_dir", direction=tag_direction)
return result
######################
# Assemblies #
######################
def trident_assembly(self) -> 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 return result
@assembly()
def shoulder_assembly(self) -> Cq.Assembly: def shoulder_assembly(self) -> Cq.Assembly:
result = ( result = (
Cq.Assembly() Cq.Assembly()
@ -415,6 +408,7 @@ class Parameters(Model):
) )
return result return result
@assembly()
def wing_r1s1_assembly(self) -> Cq.Assembly: def wing_r1s1_assembly(self) -> Cq.Assembly:
result = ( result = (
Cq.Assembly() Cq.Assembly()
@ -438,6 +432,7 @@ class Parameters(Model):
return result return result
@assembly()
def wing_r1_assembly(self) -> Cq.Assembly: def wing_r1_assembly(self) -> Cq.Assembly:
result = ( result = (
Cq.Assembly() Cq.Assembly()
@ -452,6 +447,7 @@ class Parameters(Model):
) )
return result return result
@assembly()
def wings_assembly(self) -> Cq.Assembly: def wings_assembly(self) -> Cq.Assembly:
""" """
Assembly of harness with all the wings Assembly of harness with all the wings
@ -474,6 +470,14 @@ class Parameters(Model):
) )
return result 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__': if __name__ == '__main__':
p = Parameters() p = Parameters()

View File

@ -9,11 +9,6 @@ class Test(unittest.TestCase):
p = M.Parameters() p = M.Parameters()
obj = p.hs_joint_parent() obj = p.hs_joint_parent()
self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") 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): def test_wing_root(self):
p = M.Parameters() p = M.Parameters()
@ -26,12 +21,6 @@ class Test(unittest.TestCase):
self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.xlen, 255, msg=msg)
self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg)
self.assertLess(bbox.zlen, 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): def test_trident_assembly(self):
p = M.Parameters() p = M.Parameters()
assembly = p.trident_assembly() assembly = p.trident_assembly()
@ -39,6 +28,9 @@ class Test(unittest.TestCase):
length = bbox.zlen length = bbox.zlen
self.assertGreater(length, 1300) self.assertGreater(length, 1300)
self.assertLess(length, 1700) self.assertLess(length, 1700)
#def test_assemblies(self):
# p = M.Parameters()
# p.check_all()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()