test: Check assembly collision
This commit is contained in:
parent
d43482f77d
commit
53c143e0b7
62
nhf/build.py
62
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"<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:
|
||||
"""
|
||||
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -69,6 +69,8 @@ class Handle:
|
|||
.cylinder(
|
||||
radius=self.diam / 2,
|
||||
height=length)
|
||||
.faces(">Z")
|
||||
.hole(self.diam_inner)
|
||||
)
|
||||
result.faces("<Z").tag("mate1")
|
||||
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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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("<Z").tag("base")
|
||||
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)
|
||||
#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 "<Z").workplane()
|
||||
sign = 1 if front else -1
|
||||
tag_direction = "+Y" if front else "-Y"
|
||||
for name, px, py in anchors:
|
||||
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
|
||||
|
||||
@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()
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue