cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
6 changed files with 118 additions and 43 deletions
Showing only changes of commit 53c143e0b7 - Show all commits

View File

@ -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):

View File

@ -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))

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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()