cosplay: Touhou/Houjuu Nue #4
62
nhf/build.py
62
nhf/build.py
|
@ -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):
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue