cosplay: Touhou/Houjuu Nue #4
|
@ -23,6 +23,8 @@ from colorama import Fore, Style
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
import nhf.checks as NC
|
import nhf.checks as NC
|
||||||
|
|
||||||
|
TOL=1e-6
|
||||||
|
|
||||||
class TargetKind(Enum):
|
class TargetKind(Enum):
|
||||||
|
|
||||||
STL = "stl",
|
STL = "stl",
|
||||||
|
@ -70,7 +72,7 @@ class Target:
|
||||||
if isinstance(x, Cq.Workplane):
|
if isinstance(x, Cq.Workplane):
|
||||||
x = x.val()
|
x = x.val()
|
||||||
if isinstance(x, Cq.Assembly):
|
if isinstance(x, Cq.Assembly):
|
||||||
x = x.toCompound()
|
x = x.toCompound().fuse(tol=TOL)
|
||||||
x.exportStl(path, **self.kwargs)
|
x.exportStl(path, **self.kwargs)
|
||||||
elif self.kind == TargetKind.DXF:
|
elif self.kind == TargetKind.DXF:
|
||||||
assert isinstance(x, Cq.Workplane)
|
assert isinstance(x, Cq.Workplane)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
from nhf import Role
|
from nhf import Role
|
||||||
|
@ -8,6 +8,67 @@ import nhf.utils
|
||||||
|
|
||||||
TOL = 1e-6
|
TOL = 1e-6
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Beam:
|
||||||
|
"""
|
||||||
|
A I-shaped spine with two feet
|
||||||
|
"""
|
||||||
|
|
||||||
|
foot_length: float = 40.0
|
||||||
|
foot_width: float = 20.0
|
||||||
|
foot_height: float = 5.0
|
||||||
|
spine_thickness: float = 4.0
|
||||||
|
spine_length: float = 10.0
|
||||||
|
total_height: float = 50.0
|
||||||
|
|
||||||
|
hole_diam: float = 8.0
|
||||||
|
# distance between the centres of the two holes
|
||||||
|
hole_dist: float = 24.0
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.spine_height > 0
|
||||||
|
assert self.hole_diam + self.hole_dist < self.foot_length
|
||||||
|
assert self.hole_dist - self.hole_diam >= self.spine_length
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spine_height(self):
|
||||||
|
return self.total_height - self.foot_height * 2
|
||||||
|
|
||||||
|
def foot(self) -> Cq.Workplane:
|
||||||
|
"""
|
||||||
|
just one foot
|
||||||
|
"""
|
||||||
|
dx = self.hole_dist / 2
|
||||||
|
result = (
|
||||||
|
Cq.Workplane('XZ')
|
||||||
|
.box(self.foot_length, self.foot_width, self.foot_height,
|
||||||
|
centered=(True, True, False))
|
||||||
|
.faces(">Y")
|
||||||
|
.workplane()
|
||||||
|
.pushPoints([(dx, 0), (-dx, 0)])
|
||||||
|
.hole(self.hole_diam)
|
||||||
|
)
|
||||||
|
plane = result.faces(">Y").workplane()
|
||||||
|
plane.moveTo(dx, 0).tagPlane("conn1")
|
||||||
|
plane.moveTo(-dx, 0).tagPlane("conn0")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def beam(self) -> Cq.Assembly:
|
||||||
|
beam = (
|
||||||
|
Cq.Workplane('XZ')
|
||||||
|
.box(self.spine_length, self.spine_thickness, self.spine_height)
|
||||||
|
)
|
||||||
|
h = self.spine_height / 2 + self.foot_height
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(beam, name="beam")
|
||||||
|
.add(self.foot(), name="top",
|
||||||
|
loc=Cq.Location((0, h, 0)))
|
||||||
|
.add(self.foot(), name="bot",
|
||||||
|
loc=Cq.Location((0, -h, 0), (0, 0, 1), 180))
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DiskJoint(Model):
|
class DiskJoint(Model):
|
||||||
"""
|
"""
|
||||||
|
@ -76,8 +137,18 @@ class DiskJoint(Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def housing_upper_carve_offset(self) -> float:
|
def housing_upper_carve_offset(self) -> float:
|
||||||
|
"""
|
||||||
|
Distance between the spring track and the outside of the upper housing
|
||||||
|
"""
|
||||||
return self.housing_thickness + self.disk_thickness - self.spring_height
|
return self.housing_thickness + self.disk_thickness - self.spring_height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def housing_upper_dz(self) -> float:
|
||||||
|
"""
|
||||||
|
Distance between the default upper housing location and the median line
|
||||||
|
"""
|
||||||
|
return self.total_thickness / 2 - self.housing_thickness
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def radius_spring_internal(self):
|
def radius_spring_internal(self):
|
||||||
return self.radius_spring - self.spring_thickness
|
return self.radius_spring - self.spring_thickness
|
||||||
|
@ -175,8 +246,8 @@ class DiskJoint(Model):
|
||||||
length=self.spring_tail_length,
|
length=self.spring_tail_length,
|
||||||
width=self.spring_thickness,
|
width=self.spring_thickness,
|
||||||
height=self.housing_thickness
|
height=self.housing_thickness
|
||||||
).located(Cq.Location((0, self.radius_spring_internal, 0))))
|
).located(Cq.Location((0, -self.radius_spring, 0))))
|
||||||
).rotate((0, 0, 0), (0, 0, 1), 180 + self.spring_angle - self.spring_angle_shift)
|
).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift)
|
||||||
result = (
|
result = (
|
||||||
Cq.Workplane('XY')
|
Cq.Workplane('XY')
|
||||||
.cylinder(
|
.cylinder(
|
||||||
|
@ -185,8 +256,8 @@ class DiskJoint(Model):
|
||||||
centered=(True, True, False),
|
centered=(True, True, False),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.faces(">Z").tag("mate")
|
result.faces("<Z").tag("mate")
|
||||||
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X")
|
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="-X")
|
||||||
result = result.faces(">Z").hole(self.radius_axle * 2)
|
result = result.faces(">Z").hole(self.radius_axle * 2)
|
||||||
|
|
||||||
# tube which holds the spring interior
|
# tube which holds the spring interior
|
||||||
|
@ -203,14 +274,12 @@ class DiskJoint(Model):
|
||||||
result = result.union(tube)
|
result = result.union(tube)
|
||||||
wall = (
|
wall = (
|
||||||
self.wall()
|
self.wall()
|
||||||
.rotate((0, 0, 0), (0, 0, 1), self.tongue_span)
|
.located(Cq.Location((0, 0, -self.disk_thickness-self.wall_inset)))
|
||||||
.mirror("XY")
|
|
||||||
.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset)))
|
|
||||||
)
|
)
|
||||||
result = (
|
result = (
|
||||||
result
|
result
|
||||||
|
.cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset))))
|
||||||
.union(wall, tol=TOL)
|
.union(wall, tol=TOL)
|
||||||
.cut(carve.located(Cq.Location((0, 0, self.housing_upper_carve_offset))))
|
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -256,6 +325,104 @@ class DiskJoint(Model):
|
||||||
)
|
)
|
||||||
return result.solve()
|
return result.solve()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ElbowJoint:
|
||||||
|
"""
|
||||||
|
Creates the elbow and wrist joints.
|
||||||
|
|
||||||
|
This consists of a disk joint, where each side of the joint has mounting
|
||||||
|
holes for connection to the exoskeleton. Each side 2 mounting feet on the
|
||||||
|
top and bottom, and each foot has 2 holes.
|
||||||
|
|
||||||
|
On the parent side, additional bolts are needed to clamp the two sides of
|
||||||
|
the housing together.
|
||||||
|
"""
|
||||||
|
|
||||||
|
disk_joint: DiskJoint = field(default_factory=lambda: DiskJoint(
|
||||||
|
movement_angle=60,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Distance between the child/parent arm to the centre
|
||||||
|
child_arm_radius: float = 40.0
|
||||||
|
parent_arm_radius: float = 40.0
|
||||||
|
|
||||||
|
child_beam: Beam = field(default_factory=lambda: Beam())
|
||||||
|
parent_beam: Beam = field(default_factory=lambda: Beam(
|
||||||
|
spine_thickness=8.0,
|
||||||
|
))
|
||||||
|
parent_arm_span: float = 40.0
|
||||||
|
# Angle of the beginning of the parent arm
|
||||||
|
parent_arm_angle: float = 180.0
|
||||||
|
parent_binding_hole_radius: float = 30.0
|
||||||
|
|
||||||
|
# Size of the mounting holes
|
||||||
|
hole_diam: float = 8.0
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.child_arm_radius > self.disk_joint.radius_housing
|
||||||
|
assert self.parent_arm_radius > self.disk_joint.radius_housing
|
||||||
|
self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk
|
||||||
|
assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span
|
||||||
|
assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing
|
||||||
|
|
||||||
|
def child_joint(self) -> Cq.Assembly:
|
||||||
|
angle = -self.disk_joint.tongue_span / 2
|
||||||
|
dz = self.disk_joint.disk_thickness / 2
|
||||||
|
result = (
|
||||||
|
self.child_beam.beam()
|
||||||
|
.add(self.disk_joint.disk(), name="disk",
|
||||||
|
loc=Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle))
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def parent_joint_bot(self) -> Cq.Workplane:
|
||||||
|
return self.disk_joint.housing_lower()
|
||||||
|
|
||||||
|
def parent_joint_top(self):
|
||||||
|
axial_offset = Cq.Location((self.parent_arm_radius, 0, 0))
|
||||||
|
housing_dz = self.disk_joint.housing_upper_dz
|
||||||
|
conn_h = self.parent_beam.spine_thickness
|
||||||
|
connector = (
|
||||||
|
Cq.Solid.makeCylinder(
|
||||||
|
height=conn_h,
|
||||||
|
radius=self.parent_arm_radius,
|
||||||
|
angleDegrees=self.parent_arm_span)
|
||||||
|
.cut(Cq.Solid.makeCylinder(
|
||||||
|
height=conn_h,
|
||||||
|
radius=self.disk_joint.radius_housing,
|
||||||
|
))
|
||||||
|
.located(Cq.Location((0, 0, -conn_h / 2)))
|
||||||
|
.rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2)
|
||||||
|
)
|
||||||
|
housing = self.disk_joint.housing_upper()
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(housing, name="housing",
|
||||||
|
loc=axial_offset * Cq.Location((0, 0, housing_dz)))
|
||||||
|
.add(connector, name="connector",
|
||||||
|
loc=axial_offset)
|
||||||
|
.add(self.parent_beam.beam(), name="beam")
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def assembly(self, angle: float = 0) -> Cq.Assembly:
|
||||||
|
da = self.disk_joint.tongue_span / 2
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(self.child_joint(), name="child", color=Role.CHILD.color)
|
||||||
|
.add(self.parent_joint_bot(), name="parent_bot", color=Role.CASING.color)
|
||||||
|
.add(self.parent_joint_top(), name="parent_top", color=Role.PARENT.color)
|
||||||
|
.constrain("parent_bot", "Fixed")
|
||||||
|
)
|
||||||
|
self.disk_joint.add_constraints(
|
||||||
|
result,
|
||||||
|
housing_lower="parent_bot",
|
||||||
|
housing_upper="parent_top/housing",
|
||||||
|
disk="child/disk",
|
||||||
|
angle=(0, 0, angle + da),
|
||||||
|
)
|
||||||
|
return result.solve()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
p = DiskJoint()
|
p = DiskJoint()
|
||||||
p.build_all()
|
p.build_all()
|
||||||
|
|
|
@ -290,7 +290,6 @@ class WingProfile:
|
||||||
def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]:
|
def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]:
|
||||||
elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s
|
elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s
|
||||||
elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c
|
elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c
|
||||||
print(f"c={self.elbow_c}, s={self.elbow_s}, x={elbow_x}, y={elbow_y}")
|
|
||||||
return elbow_x, elbow_y
|
return elbow_x, elbow_y
|
||||||
def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]:
|
def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]:
|
||||||
wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s
|
wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s
|
||||||
|
|
Loading…
Reference in New Issue