cosplay: Touhou/Houjuu Nue #4
|
@ -1,6 +1,30 @@
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
|
|
||||||
def binary_intersection(a: Cq.Assembly) -> Cq.Shape:
|
def binary_intersection(a: Cq.Assembly) -> Cq.Shape:
|
||||||
objs = [s.toCompound() for _, s in a.traverse() if isinstance(s, Cq.Assembly)]
|
objs = [s.toCompound() for _, s in a.traverse()
|
||||||
|
if isinstance(s, Cq.Assembly)]
|
||||||
obj1, obj2 = objs[:2]
|
obj1, obj2 = objs[:2]
|
||||||
return obj1.intersect(obj2)
|
return obj1.intersect(obj2)
|
||||||
|
|
||||||
|
|
||||||
|
def pairwise_intersection(assembly: Cq.Assembly, tol: float=1e-6) -> list[(str, str, float)]:
|
||||||
|
"""
|
||||||
|
Given an assembly, test the pairwise intersection volume of its components.
|
||||||
|
Return the pairs whose intersection volume exceeds `tol`.
|
||||||
|
"""
|
||||||
|
m = {name: (i, shape.moved(loc))
|
||||||
|
for i, (shape, name, loc, _)
|
||||||
|
in enumerate(assembly)}
|
||||||
|
result = []
|
||||||
|
for name, (i1, sh1) in m.items():
|
||||||
|
for name2, (i2, sh2) in m.items():
|
||||||
|
if name == name2:
|
||||||
|
assert i1 == i2
|
||||||
|
continue
|
||||||
|
if i2 <= i1:
|
||||||
|
# Remove the upper diagonal
|
||||||
|
continue
|
||||||
|
vol = sh1.intersect(sh2).Volume()
|
||||||
|
if vol > tol:
|
||||||
|
result.append((name, name2, vol))
|
||||||
|
return result
|
||||||
|
|
|
@ -31,6 +31,7 @@ class Material(Enum):
|
||||||
|
|
||||||
WOOD_BIRCH = 0.8, _color('bisque', 0.9)
|
WOOD_BIRCH = 0.8, _color('bisque', 0.9)
|
||||||
PLASTIC_PLA = 0.5, _color('azure3', 0.6)
|
PLASTIC_PLA = 0.5, _color('azure3', 0.6)
|
||||||
|
RESIN_TRANSPARENT = 1.1, _color('cadetblue2', 0.6)
|
||||||
ACRYLIC_BLACK = 0.5, _color('gray50', 0.6)
|
ACRYLIC_BLACK = 0.5, _color('gray50', 0.6)
|
||||||
|
|
||||||
def __init__(self, density: float, color: Cq.Color):
|
def __init__(self, density: float, color: Cq.Color):
|
||||||
|
|
127
nhf/test.py
127
nhf/test.py
|
@ -1,9 +1,27 @@
|
||||||
|
"""
|
||||||
|
Unit tests for tooling
|
||||||
|
"""
|
||||||
import unittest
|
import unittest
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
from nhf.build import Model, target
|
from nhf.build import Model, target
|
||||||
|
import nhf.checks
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
# Color presets for testing purposes
|
||||||
|
color_parent = Cq.Color(0.7, 0.7, 0.5, 0.5)
|
||||||
|
color_child = Cq.Color(0.5, 0.7, 0.7, 0.5)
|
||||||
|
|
||||||
|
def makeSphere(r: float) -> Cq.Solid:
|
||||||
|
"""
|
||||||
|
Makes a full sphere. The default function makes a hemisphere
|
||||||
|
"""
|
||||||
|
return Cq.Solid.makeSphere(r, angleDegrees1=-90)
|
||||||
|
|
||||||
class BuildScaffold(Model):
|
class BuildScaffold(Model):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(name="scaffold")
|
||||||
|
|
||||||
@target(name="obj1")
|
@target(name="obj1")
|
||||||
def o1(self):
|
def o1(self):
|
||||||
return Cq.Solid.makeBox(10, 10, 10)
|
return Cq.Solid.makeBox(10, 10, 10)
|
||||||
|
@ -19,6 +37,115 @@ class TestBuild(unittest.TestCase):
|
||||||
self.assertEqual(s.target_names, names)
|
self.assertEqual(s.target_names, names)
|
||||||
self.assertEqual(s.check_all(), len(names))
|
self.assertEqual(s.check_all(), len(names))
|
||||||
|
|
||||||
|
class TestChecks(unittest.TestCase):
|
||||||
|
|
||||||
|
def intersect_test_case(self, offset):
|
||||||
|
assembly = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(Cq.Solid.makeBox(10, 10, 10),
|
||||||
|
name="c1",
|
||||||
|
loc=Cq.Location((0, 0, 0)))
|
||||||
|
.add(Cq.Solid.makeBox(10, 10, 10),
|
||||||
|
name="c2",
|
||||||
|
loc=Cq.Location((0, 0, offset)))
|
||||||
|
)
|
||||||
|
coll = nhf.checks.pairwise_intersection(assembly)
|
||||||
|
if -10 < offset and offset < 10:
|
||||||
|
self.assertEqual(len(coll), 1)
|
||||||
|
else:
|
||||||
|
self.assertEqual(coll, [])
|
||||||
|
|
||||||
|
def test_intersect(self):
|
||||||
|
for offset in [9, 10, 11, -10]:
|
||||||
|
with self.subTest(offset=offset):
|
||||||
|
self.intersect_test_case(offset)
|
||||||
|
|
||||||
|
class TestUtils(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_tag_point(self):
|
||||||
|
"""
|
||||||
|
A board with 3 holes of unequal sizes. Each hole is marked
|
||||||
|
"""
|
||||||
|
p4x, p4y = 5, 5
|
||||||
|
p3x, p3y = 0, 0
|
||||||
|
p2x, p2y = -5, 0
|
||||||
|
board = (
|
||||||
|
Cq.Workplane('XY')
|
||||||
|
.box(15, 15, 5)
|
||||||
|
.faces("<Z")
|
||||||
|
.workplane()
|
||||||
|
.pushPoints([(p4x, p4y)])
|
||||||
|
.hole(4, depth=2)
|
||||||
|
.pushPoints([(p3x, p3y)])
|
||||||
|
.hole(3, depth=1.5)
|
||||||
|
.pushPoints([(p2x, p2y)])
|
||||||
|
.hole(2, depth=1)
|
||||||
|
)
|
||||||
|
board.moveTo(p4x, p4y).tagPoint("h4")
|
||||||
|
board.moveTo(p3x, p3y).tagPoint("h3")
|
||||||
|
board.moveTo(p2x, p2y).tagPoint("h2")
|
||||||
|
|
||||||
|
assembly = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(board, name="board", color=color_parent)
|
||||||
|
.add(makeSphere(2), name="s4", color=color_child)
|
||||||
|
.add(makeSphere(1.5), name="s3", color=color_child)
|
||||||
|
.add(makeSphere(1), name="s2", color=color_child)
|
||||||
|
.constrain("board?h4", "s4", "Point")
|
||||||
|
.constrain("board?h3", "s3", "Point")
|
||||||
|
.constrain("board?h2", "s2", "Point")
|
||||||
|
.solve()
|
||||||
|
)
|
||||||
|
self.assertEqual(nhf.checks.pairwise_intersection(assembly), [])
|
||||||
|
bbox = assembly.toCompound().BoundingBox()
|
||||||
|
self.assertAlmostEqual(bbox.xlen, 15)
|
||||||
|
self.assertAlmostEqual(bbox.ylen, 15)
|
||||||
|
self.assertAlmostEqual(bbox.zlen, 7)
|
||||||
|
|
||||||
|
def test_tag_plane(self):
|
||||||
|
p4x, p4y = 5, 5
|
||||||
|
p3x, p3y = 0, 0
|
||||||
|
p2x, p2y = -5, 0
|
||||||
|
board = (
|
||||||
|
Cq.Workplane('XY')
|
||||||
|
.box(15, 15, 5)
|
||||||
|
.faces("<Z")
|
||||||
|
.workplane()
|
||||||
|
.pushPoints([(p4x, p4y)])
|
||||||
|
.hole(4, depth=2)
|
||||||
|
.pushPoints([(p3x, p3y)])
|
||||||
|
.hole(3, depth=1.5)
|
||||||
|
.pushPoints([(p2x, p2y)])
|
||||||
|
.hole(2, depth=1)
|
||||||
|
)
|
||||||
|
board.moveTo(p4x, p4y).tagPlane("h4")
|
||||||
|
board.moveTo(p3x, p3y).tagPlane("h3")
|
||||||
|
board.moveTo(p2x, p2y).tagPlane("h2")
|
||||||
|
|
||||||
|
def markedCylOf(r):
|
||||||
|
cyl = (
|
||||||
|
Cq.Workplane('XY')
|
||||||
|
.cylinder(radius=r, height=r)
|
||||||
|
)
|
||||||
|
cyl.faces("<Z").tag("mate")
|
||||||
|
return cyl
|
||||||
|
|
||||||
|
assembly = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(board, name="board", color=color_parent)
|
||||||
|
.add(markedCylOf(2), name="c4", color=color_child)
|
||||||
|
.add(markedCylOf(1.5), name="c3", color=color_child)
|
||||||
|
.add(markedCylOf(1), name="c2", color=color_child)
|
||||||
|
.constrain("board?h4", "c4?mate", "Plane", param=0)
|
||||||
|
.constrain("board?h3", "c3?mate", "Plane", param=0)
|
||||||
|
.constrain("board?h2", "c2?mate", "Plane", param=0)
|
||||||
|
.solve()
|
||||||
|
)
|
||||||
|
self.assertEqual(nhf.checks.pairwise_intersection(assembly), [])
|
||||||
|
bbox = assembly.toCompound().BoundingBox()
|
||||||
|
self.assertAlmostEqual(bbox.xlen, 15)
|
||||||
|
self.assertAlmostEqual(bbox.ylen, 15)
|
||||||
|
self.assertAlmostEqual(bbox.zlen, 5)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -34,10 +34,11 @@ 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
|
||||||
from nhf.parts.joints import HirthJoint
|
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
|
||||||
import nhf.touhou.houjuu_nue.trident as MT
|
import nhf.touhou.houjuu_nue.trident as MT
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Parameters(Model):
|
class Parameters(Model):
|
||||||
|
@ -89,6 +90,16 @@ class Parameters(Model):
|
||||||
|
|
||||||
# Exterior radius of the wing root assembly
|
# Exterior radius of the wing root assembly
|
||||||
wing_root_radius: float = 40
|
wing_root_radius: float = 40
|
||||||
|
wing_root_wall_thickness: float = 8
|
||||||
|
|
||||||
|
shoulder_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
|
||||||
|
radius_axle=8
|
||||||
|
))
|
||||||
|
|
||||||
|
# Two holes on each side (top and bottom) are used to attach the shoulder
|
||||||
|
# joint. This governs the distance between these two holes
|
||||||
|
shoulder_attach_dist: float = 25
|
||||||
|
shoulder_attach_diam: float = 8
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Heights for various wing joints, where the numbers start from the first joint.
|
Heights for various wing joints, where the numbers start from the first joint.
|
||||||
|
@ -164,7 +175,8 @@ class Parameters(Model):
|
||||||
result.faces(">Y").tag("mount")
|
result.faces(">Y").tag("mount")
|
||||||
plane = result.faces(">Y").workplane()
|
plane = result.faces(">Y").workplane()
|
||||||
for tag, x, y in self.harness_wing_base_pos:
|
for tag, x, y in self.harness_wing_base_pos:
|
||||||
conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()]
|
conn = [(px + x, py + y) for px, py
|
||||||
|
in self.hs_joint_harness_conn()]
|
||||||
for i, (px, py) in enumerate(conn):
|
for i, (px, py) in enumerate(conn):
|
||||||
(
|
(
|
||||||
plane
|
plane
|
||||||
|
@ -250,7 +262,30 @@ class Parameters(Model):
|
||||||
Generate the wing root which contains a Hirth joint at its base and a
|
Generate the wing root which contains a Hirth joint at its base and a
|
||||||
rectangular opening on its side, with the necessary interfaces.
|
rectangular opening on its side, with the necessary interfaces.
|
||||||
"""
|
"""
|
||||||
return MW.wing_root(joint=self.hs_hirth_joint)
|
return MW.wing_root(
|
||||||
|
joint=self.hs_hirth_joint,
|
||||||
|
shoulder_attach_dist=self.shoulder_attach_dist,
|
||||||
|
shoulder_attach_diam=self.shoulder_attach_diam,
|
||||||
|
wall_thickness=self.wing_root_wall_thickness,
|
||||||
|
)
|
||||||
|
|
||||||
|
@target(name="shoulder")
|
||||||
|
def shoulder_parent_joint(self) -> Cq.Assembly:
|
||||||
|
result = (
|
||||||
|
self.shoulder_joint.rider()
|
||||||
|
.copyWorkplane(Cq.Workplane(
|
||||||
|
'YZ', origin=Cq.Vector((100, 0, self.wing_root_wall_thickness))))
|
||||||
|
.rect(30, 10, centered=(True, False))
|
||||||
|
.extrude("next")
|
||||||
|
.copyWorkplane(Cq.Workplane(
|
||||||
|
'YX', origin=Cq.Vector((60, 0, self.wing_root_wall_thickness))))
|
||||||
|
.hole(self.shoulder_attach_diam)
|
||||||
|
.moveTo(0, self.shoulder_attach_dist)
|
||||||
|
.hole(self.shoulder_attach_diam)
|
||||||
|
)
|
||||||
|
result.moveTo(0, 0).tagPlane('conn0')
|
||||||
|
result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1')
|
||||||
|
return result
|
||||||
|
|
||||||
def wing_r1_profile(self) -> Cq.Sketch:
|
def wing_r1_profile(self) -> Cq.Sketch:
|
||||||
"""
|
"""
|
||||||
|
@ -295,10 +330,10 @@ class Parameters(Model):
|
||||||
# Assemblies #
|
# Assemblies #
|
||||||
######################
|
######################
|
||||||
|
|
||||||
def trident_assembly(self):
|
def trident_assembly(self) -> Cq.Assembly:
|
||||||
return MT.trident_assembly(self.trident_handle)
|
return MT.trident_assembly(self.trident_handle)
|
||||||
|
|
||||||
def harness_assembly(self):
|
def harness_assembly(self) -> Cq.Assembly:
|
||||||
harness = self.harness()
|
harness = self.harness()
|
||||||
result = (
|
result = (
|
||||||
Cq.Assembly()
|
Cq.Assembly()
|
||||||
|
@ -317,7 +352,19 @@ class Parameters(Model):
|
||||||
result.solve()
|
result.solve()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def wings_assembly(self):
|
def wing_r1_assembly(self) -> Cq.Assembly:
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(self.wing_root(), name="r1")
|
||||||
|
.add(self.shoulder_parent_joint(), name="shoulder_parent_top",
|
||||||
|
color=Material.RESIN_TRANSPARENT.color)
|
||||||
|
.constrain("r1/scaffold?conn_top0", "shoulder_parent_top?conn0", "Plane")
|
||||||
|
.constrain("r1/scaffold?conn_top1", "shoulder_parent_top?conn1", "Plane")
|
||||||
|
.solve()
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def wings_assembly(self) -> Cq.Assembly:
|
||||||
"""
|
"""
|
||||||
Assembly of harness with all the wings
|
Assembly of harness with all the wings
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -6,6 +6,7 @@ import math
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
from nhf import Material, Role
|
from nhf import Material, Role
|
||||||
from nhf.parts.joints import HirthJoint
|
from nhf.parts.joints import HirthJoint
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
def wing_root_profiles(
|
def wing_root_profiles(
|
||||||
base_sweep=150,
|
base_sweep=150,
|
||||||
|
@ -129,7 +130,8 @@ def wing_root_profiles(
|
||||||
def wing_root(joint: HirthJoint,
|
def wing_root(joint: HirthJoint,
|
||||||
bolt_diam: int = 12,
|
bolt_diam: int = 12,
|
||||||
union_tol=1e-4,
|
union_tol=1e-4,
|
||||||
attach_diam=8,
|
shoulder_attach_diam=8,
|
||||||
|
shoulder_attach_dist=25,
|
||||||
conn_width=40,
|
conn_width=40,
|
||||||
conn_height=100,
|
conn_height=100,
|
||||||
wall_thickness=8) -> Cq.Assembly:
|
wall_thickness=8) -> Cq.Assembly:
|
||||||
|
@ -139,7 +141,7 @@ def wing_root(joint: HirthJoint,
|
||||||
tip_centre = Cq.Vector((-150, 0, -80))
|
tip_centre = Cq.Vector((-150, 0, -80))
|
||||||
attach_points = [
|
attach_points = [
|
||||||
(15, 0),
|
(15, 0),
|
||||||
(40, 0),
|
(15 + shoulder_attach_dist, 0),
|
||||||
]
|
]
|
||||||
root_profile, middle_profile, tip_profile = wing_root_profiles(
|
root_profile, middle_profile, tip_profile = wing_root_profiles(
|
||||||
conn_width=conn_width,
|
conn_width=conn_width,
|
||||||
|
@ -183,7 +185,7 @@ def wing_root(joint: HirthJoint,
|
||||||
Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0)))
|
Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0)))
|
||||||
)
|
)
|
||||||
.pushPoints(attach_points)
|
.pushPoints(attach_points)
|
||||||
.hole(attach_diam)
|
.hole(shoulder_attach_diam)
|
||||||
)
|
)
|
||||||
# Generate attach point tags
|
# Generate attach point tags
|
||||||
|
|
||||||
|
@ -200,12 +202,8 @@ def wing_root(joint: HirthJoint,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for i, (px, py) in enumerate(attach_points):
|
for i, (px, py) in enumerate(attach_points):
|
||||||
(
|
tag = f"conn_{side}{i}"
|
||||||
plane
|
plane.moveTo(px, py).tagPlane(tag)
|
||||||
.moveTo(px, py)
|
|
||||||
.eachpoint(Cq.Vertex.makeVertex(0, 0, 0))
|
|
||||||
.tag(f"conn_{side}{i}")
|
|
||||||
)
|
|
||||||
|
|
||||||
result.faces("<Z").tag("base")
|
result.faces("<Z").tag("base")
|
||||||
result.faces(">X").tag("conn")
|
result.faces(">X").tag("conn")
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""
|
||||||
|
Marking utilities for `Cq.Workplane`
|
||||||
|
|
||||||
|
Adds the functions to `Cq.Workplane`:
|
||||||
|
1. `tagPoint`
|
||||||
|
2. `tagPlane`
|
||||||
|
"""
|
||||||
|
import cadquery as Cq
|
||||||
|
|
||||||
|
|
||||||
|
def tagPoint(self, tag: str):
|
||||||
|
"""
|
||||||
|
Adds a vertex that can be used in `Point` constraints.
|
||||||
|
"""
|
||||||
|
vertex = Cq.Vertex.makeVertex(0, 0, 0)
|
||||||
|
self.eachpoint(vertex.moved, useLocalCoordinates=True).tag(tag)
|
||||||
|
|
||||||
|
Cq.Workplane.tagPoint = tagPoint
|
||||||
|
|
||||||
|
|
||||||
|
def tagPlane(self, tag: str, axis='Z'):
|
||||||
|
"""
|
||||||
|
Adds a phantom `Cq.Edge` in the given location which can be referenced in a
|
||||||
|
`Axis`, `Point`, or `Plane` constraint.
|
||||||
|
"""
|
||||||
|
x, y, z = 0, 0, 0
|
||||||
|
if axis in ('z', 'Z'):
|
||||||
|
z = 1
|
||||||
|
elif axis in ('y', 'Y'):
|
||||||
|
y = 1
|
||||||
|
elif axis in ('x', 'X'):
|
||||||
|
x = 1
|
||||||
|
else:
|
||||||
|
assert False, "Axis must be one of x,y,z"
|
||||||
|
edge = Cq.Edge.makeLine((-x, -y, -z), (x, y, z))
|
||||||
|
self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag)
|
||||||
|
|
||||||
|
Cq.Workplane.tagPlane = tagPlane
|
Loading…
Reference in New Issue