cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
5 changed files with 281 additions and 207 deletions
Showing only changes of commit 4b6b05853e - Show all commits

View File

@ -122,5 +122,5 @@ class HexNut(Item):
)
result.faces("<Z").tag("bot")
result.faces(">Z").tag("top")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dirX", direction="+X")
return result

View File

@ -77,22 +77,15 @@ class Parameters(Model):
def __post_init__(self):
super().__init__(name="houjuu-nue")
self.wing_r1.base_joint = self.harness.hs_hirth_joint
self.wing_r2.base_joint = self.harness.hs_hirth_joint
self.wing_r3.base_joint = self.harness.hs_hirth_joint
self.wing_l1.base_joint = self.harness.hs_hirth_joint
self.wing_l2.base_joint = self.harness.hs_hirth_joint
self.wing_l3.base_joint = self.harness.hs_hirth_joint
self.wing_r1.root_joint = self.harness.root_joint
self.wing_r2.root_joint = self.harness.root_joint
self.wing_r3.root_joint = self.harness.root_joint
self.wing_l1.root_joint = self.harness.root_joint
self.wing_l2.root_joint = self.harness.root_joint
self.wing_l3.root_joint = self.harness.root_joint
self.wing_r1.shoulder_joint.torsion_joint
assert self.wing_r1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam
assert self.wing_r2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam
assert self.wing_r3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam
assert self.wing_l1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam
assert self.wing_l2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam
assert self.wing_l3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam
@submodel(name="harness")
def submodel_harness(self) -> Model:
return self.harness
@ -126,19 +119,20 @@ class Parameters(Model):
result = (
Cq.Assembly()
.add(self.harness.assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
.add(self.wing_r1.assembly(parts, **kwargs), name="wing_r1")
.add(self.wing_r2.assembly(parts, **kwargs), name="wing_r2")
.add(self.wing_r3.assembly(parts, **kwargs), name="wing_r3")
.add(self.wing_l1.assembly(parts, **kwargs), name="wing_l1")
.add(self.wing_l2.assembly(parts, **kwargs), name="wing_l2")
.add(self.wing_l3.assembly(parts, **kwargs), name="wing_l3")
.add(self.wing_r1.assembly(parts, root_offset=9, **kwargs), name="wing_r1")
.add(self.wing_r2.assembly(parts, root_offset=7, **kwargs), name="wing_r2")
.add(self.wing_r3.assembly(parts, root_offset=6, **kwargs), name="wing_r3")
.add(self.wing_l1.assembly(parts, root_offset=7, **kwargs), name="wing_l1")
.add(self.wing_l2.assembly(parts, root_offset=8, **kwargs), name="wing_l2")
.add(self.wing_l3.assembly(parts, root_offset=9, **kwargs), name="wing_l3")
)
for tag, offset in [("r1", 9), ("r2", 7), ("r3", 6), ("l1", 7), ("l2", 8), ("l3", 9)]:
self.harness.hs_hirth_joint.add_constraints(
for tag in ["r1", "r2", "r3", "l1", "l2", "l3"]:
self.harness.add_root_joint_constraint(
result,
f"harness/{tag}",
f"wing_{tag}/s0/hs",
offset=offset)
"harness/base",
f"wing_{tag}/root",
tag
)
return result.solve()
@submodel(name="trident")

View File

@ -2,7 +2,8 @@ from dataclasses import dataclass, field
import cadquery as Cq
from nhf.parts.joints import HirthJoint
from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.touhou.houjuu_nue.joints import RootJoint
import nhf.utils
@dataclass(frozen=True, kw_only=True)
@ -63,33 +64,18 @@ class Harness(Model):
("r3", BASE_POS_X, -BASE_POS_Y),
("l3", -BASE_POS_X, -BASE_POS_Y),
])
# Holes drilled onto harness for attachment with HS joint
harness_to_root_conn_diam: float = 6
hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint(
radius=25.0,
radius_inner=15.0,
tooth_height=7.0,
base_height=5.0,
n_tooth=24,
))
hs_joint_base_width: float = 85
hs_joint_base_thickness: float = 10
hs_joint_corner_fillet: float = 5
hs_joint_corner_cbore_diam: float = 12
hs_joint_corner_cbore_depth: float = 2
hs_joint_corner_inset: float = 12
hs_joint_axis_diam: float = 12.0
hs_joint_axis_cbore_diam: float = 20
hs_joint_axis_cbore_depth: float = 3
root_joint: RootJoint = field(default_factory=lambda: RootJoint())
mannequin: Mannequin = Mannequin()
def __post_init__(self):
super().__init__(name="harness")
@submodel(name="root-joint")
def submodel_root_joint(self) -> Model:
return self.root_joint
@target(name="profile", kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch:
"""
@ -115,12 +101,12 @@ class Harness(Model):
.fillet(self.fillet)
)
for tag, x, y in self.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.root_joint.corner_pos()]
sketch = (
sketch
.push(conn)
.tag(tag)
.circle(self.harness_to_root_conn_diam / 2, mode='s')
.circle(self.root_joint.corner_hole_diam / 2, mode='s')
.reset()
)
return sketch
@ -138,78 +124,27 @@ class Harness(Model):
plane = result.faces(">Y").workplane()
for tag, x, y in self.wing_base_pos:
conn = [(px + x, py + y) for px, py
in self.hs_joint_harness_conn()]
in self.root_joint.corner_pos()]
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPoint(f"{tag}_{i}")
return result
def hs_joint_harness_conn(self) -> list[tuple[int, int]]:
"""
Generates a set of points corresponding to the connectorss
"""
dx = self.hs_joint_base_width / 2 - self.hs_joint_corner_inset
return [
(dx, dx),
(dx, -dx),
(-dx, -dx),
(-dx, dx),
]
def add_root_joint_constraint(
self,
a: Cq.Assembly,
harness_tag: str,
joint_tag: str,
mount_tag: str):
for i in range(4):
a.constrain(f"{harness_tag}?{mount_tag}_{i}", f"{joint_tag}/parent?h{i}", "Point")
@target(name="hs-joint-parent")
def hs_joint_parent(self):
"""
Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth
coupling, a cylindrical base, and a mounting base.
"""
hirth = self.hs_hirth_joint.generate()
conn = self.hs_joint_harness_conn()
result = (
Cq.Workplane('XY')
.box(
self.hs_joint_base_width,
self.hs_joint_base_width,
self.hs_joint_base_thickness,
centered=(True, True, False))
.translate((0, 0, -self.hs_joint_base_thickness))
.edges("|Z")
.fillet(self.hs_joint_corner_fillet)
.faces(">Z")
.workplane()
.pushPoints(conn)
.cboreHole(
diameter=self.harness_to_root_conn_diam,
cboreDiameter=self.hs_joint_corner_cbore_diam,
cboreDepth=self.hs_joint_corner_cbore_depth)
)
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness)
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPoint(f"h{i}")
result = (
result
.faces(">Z")
.workplane()
.union(hirth, tol=0.1)
.clean()
)
result = (
result.faces("<Z")
.workplane()
.cboreHole(
diameter=self.hs_joint_axis_diam,
cboreDiameter=self.hs_joint_axis_cbore_diam,
cboreDepth=self.hs_joint_axis_cbore_depth,
)
.clean()
)
result.faces("<Z").tag("base")
return result
@assembly()
def assembly(self) -> Cq.Assembly:
def assembly(self, with_root_joint: bool = False) -> Cq.Assembly:
harness = self.surface()
mannequin_z = self.mannequin.shoulder_to_waist * 0.6
result = (
Cq.Assembly()
.addS(
@ -224,13 +159,12 @@ class Harness(Model):
loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180))
.constrain("mannequin", "Fixed")
)
if with_root_joint:
for name in ["l1", "l2", "l3", "r1", "r2", "r3"]:
j = self.hs_joint_parent()
result.addS(
j, name=name,
self.root_joint.assembly(), name=name,
role=Role.PARENT,
material=Material.PLASTIC_PLA)
for i in range(4):
result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point")
self.add_root_joint_constraint(result, "base", name, name)
result.solve()
return result

View File

@ -5,13 +5,204 @@ import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt
from nhf.parts.joints import TorsionJoint
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.joints import TorsionJoint, HirthJoint
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
import nhf.utils
TOL = 1e-6
@dataclass
class RootJoint(Model):
"""
The Houjuu-Scarlett Mechanism
"""
knob: ThreaddedKnob = ThreaddedKnob(
mass=float('nan'),
diam_thread=12.0,
height_thread=30.0,
diam_knob=50.0,
# FIXME: Undetermined
diam_neck=30.0,
height_neck=10.0,
height_knob=10.0,
)
hex_nut: HexNut = HexNut(
# FIXME: Undetermined
mass=float('nan'),
diam_thread=12.0,
pitch=1.75,
thickness=9.8,
width=18.9,
)
hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint(
radius=25.0,
radius_inner=15.0,
tooth_height=7.0,
base_height=5.0,
n_tooth=24,
))
parent_width: float = 85
parent_thickness: float = 10
parent_corner_fillet: float = 5
parent_corner_cbore_diam: float = 12
parent_corner_cbore_depth: float = 2
parent_corner_inset: float = 12
parent_mount_thickness: float = 25.4 / 16
child_corner_dx: float = 17.0
child_corner_dz: float = 24.0
axis_diam: float = 12.0
axis_cbore_diam: float = 20
axis_cbore_depth: float = 3
corner_hole_diam: float = 6.0
child_height: float = 60.0
child_width: float = 50.0
child_mount_thickness: float = 25.4 / 8
def corner_pos(self) -> list[tuple[int, int]]:
"""
Generates a set of points corresponding to the connectorss
"""
dx = self.parent_width / 2 - self.parent_corner_inset
return [
(dx, dx),
(dx, -dx),
(-dx, -dx),
(-dx, dx),
]
@property
def total_height(self) -> float:
return self.parent_thickness + self.hirth_joint.total_height
@target(name="parent")
def parent(self):
"""
Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth
coupling, a cylindrical base, and a mounting base.
"""
hirth = self.hirth_joint.generate()
conn = self.corner_pos()
result = (
Cq.Workplane('XY')
.box(
self.parent_width,
self.parent_width,
self.parent_thickness,
centered=(True, True, False))
.translate((0, 0, -self.parent_thickness))
.edges("|Z")
.fillet(self.parent_corner_fillet)
.faces(">Z")
.workplane()
.pushPoints(conn)
.cboreHole(
diameter=self.corner_hole_diam,
cboreDiameter=self.parent_corner_cbore_diam,
cboreDepth=self.parent_corner_cbore_depth)
)
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.parent_thickness)
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPoint(f"h{i}")
result = (
result
.faces(">Z")
.workplane()
.union(hirth, tol=0.1)
.clean()
)
result = (
result.faces("<Z")
.workplane()
.hole(diameter=self.axis_diam)
.cut(self.hex_nut.generate().translate((0, 0, -self.parent_thickness)))
)
result.faces("<Z").tag("base")
return result
@target(name="child")
def child(self) -> Cq.Workplane:
hirth = self.hirth_joint.generate(is_mated=True)
dy = self.child_corner_dx
dx = self.child_corner_dz
conn = [
(-dx, -dy),
(dx, -dy),
(dx, dy),
(-dx, dy),
]
result = (
Cq.Workplane('XY')
.box(
self.child_height,
self.child_width,
self.hirth_joint.base_height,
centered=(True, True, False))
#.translate((0, 0, -self.base_joint.base_height))
#.edges("|Z")
#.fillet(self.hs_joint_corner_fillet)
.faces(">Z")
.workplane()
.pushPoints(conn)
.hole(self.corner_hole_diam)
)
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.hirth_joint.base_height)
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPlane(f"conn{i}")
result = (
result
.faces(">Z")
.workplane()
.union(hirth, tol=0.1)
.clean()
)
result = (
result.faces("<Z")
.workplane()
.hole(self.axis_diam)
)
return result
def assembly(self,
offset: int = 0,
fastener_pos: float = 0) -> Cq.Assembly:
"""
Specify knob position to determine the position of the knob from fully
inserted (0) or fully uninserted (1)
"""
knob_h = self.total_height + self.child_mount_thickness
result = (
Cq.Assembly()
.addS(self.parent(), name="parent",
material=Material.PLASTIC_PLA,
role=Role.PARENT)
.constrain("parent", "Fixed")
.addS(self.child(), name="child",
material=Material.PLASTIC_PLA,
role=Role.CHILD)
.addS(self.hex_nut.assembly(), name="hex_nut")
.addS(self.knob.assembly(), name="knob",
loc=Cq.Location((0, 0, knob_h * fastener_pos)))
.constrain("knob/thread", "Fixed")
.constrain("hex_nut?bot", "parent?base", "Plane", param=0)
.constrain("hex_nut?dirX", "parent@faces@>X", "Axis", param=0)
)
self.hirth_joint.add_constraints(
result,
"parent",
"child",
offset=offset
)
return result.solve()
@dataclass
class ShoulderJoint(Model):
@ -228,10 +419,11 @@ class ShoulderJoint(Model):
return result
@assembly()
def assembly(self, deflection: float = 0) -> Cq.Assembly:
def assembly(self, fastener_pos: float = 0.0, deflection: float = 0) -> Cq.Assembly:
directrix = self.directrix_id
mat = Material.RESIN_TRANSPERENT
mat_spring = Material.STEEL_SPRING
bolt_z = self.height / 2 + self.bolt.height_thread * (fastener_pos - 1)
result = (
Cq.Assembly()
.addS(self.child(), name="child",
@ -239,18 +431,21 @@ class ShoulderJoint(Model):
.constrain("child/core", "Fixed")
.addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top",
role=Role.DAMPING, material=mat_spring)
.addS(self.bolt.assembly(), name="bolt_top")
.addS(self.parent_top(),
name="parent_top",
role=Role.PARENT, material=mat)
.addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot",
role=Role.DAMPING, material=mat_spring)
.addS(self.bolt.assembly(), name="bolt_bot")
.addS(self.parent_bot(),
name="parent_bot",
role=Role.PARENT, material=mat)
.constrain("bolt_top/thread?root", "parent_top/track?bot", "Plane", param=0)
.constrain("bolt_bot/thread?root", "parent_bot/track?bot", "Plane", param=0)
# Fasteners
.addS(self.bolt.assembly(), name="bolt_top",
loc=Cq.Location((0, 0, bolt_z)))
.constrain("bolt_top/thread?root", 'Fixed')
.addS(self.bolt.assembly(), name="bolt_bot",
loc=Cq.Location((0, 0, -bolt_z), (1,0,0), 180))
.constrain("bolt_bot/thread?root", 'Fixed')
)
TorsionJoint.add_constraints(
result,

View File

@ -11,7 +11,7 @@ from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.parts.box import box_with_centre_holes, MountingBox, Hole
from nhf.parts.joints import HirthJoint
from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint, DiskJoint
from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint
from nhf.parts.planar import extrude_with_markers
import nhf.utils
@ -20,19 +20,8 @@ class WingProfile(Model):
name: str = "wing"
base_joint: HirthJoint = field(default_factory=lambda: HirthJoint(
radius=25.0,
radius_inner=15.0,
tooth_height=7.0,
base_height=5,
n_tooth=24,
))
root_joint: RootJoint = field(default_factory=lambda: RootJoint())
base_width: float = 80.0
hs_joint_corner_dx: float = 17.0
hs_joint_corner_dz: float = 24.0
hs_joint_corner_hole_diam: float = 6.0
hs_joint_axis_diam: float = 12.0
base_plate_width: float = 50.0
panel_thickness: float = 25.4 / 16
spacer_thickness: float = 25.4 / 8
@ -108,6 +97,8 @@ class WingProfile(Model):
self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias
self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, -self.shoulder_angle_bias)
assert self.spacer_thickness == self.root_joint.child_mount_thickness
@submodel(name="shoulder-joint")
def submodel_shoulder_joint(self) -> Model:
return self.shoulder_joint
@ -126,55 +117,6 @@ class WingProfile(Model):
def shoulder_height(self) -> float:
return self.shoulder_joint.height
@target(name="base-hs-joint")
def base_hs_joint(self) -> Cq.Workplane:
"""
Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth
coupling, a cylindrical base, and a mounting base.
"""
hirth = self.base_joint.generate(is_mated=True)
dy = self.hs_joint_corner_dx
dx = self.hs_joint_corner_dz
conn = [
(-dx, -dy),
(dx, -dy),
(dx, dy),
(-dx, dy),
]
result = (
Cq.Workplane('XY')
.box(
self.root_height,
self.base_plate_width,
self.base_joint.base_height,
centered=(True, True, False))
#.translate((0, 0, -self.base_joint.base_height))
#.edges("|Z")
#.fillet(self.hs_joint_corner_fillet)
.faces(">Z")
.workplane()
.pushPoints(conn)
.hole(self.hs_joint_corner_hole_diam)
)
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.base_joint.base_height)
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPlane(f"conn{i}")
result = (
result
.faces(">Z")
.workplane()
.union(hirth, tol=0.1)
.clean()
)
result = (
result.faces("<Z")
.workplane()
.hole(self.hs_joint_axis_diam)
)
return result
def outer_profile_s0(self) -> Cq.Sketch:
"""
The outer boundary of s0, used to produce the curved panel and the
@ -275,11 +217,11 @@ class WingProfile(Model):
"""
Should be cut
"""
assert self.base_plate_width < self.base_width
assert self.hs_joint_corner_dx * 2 < self.base_width
assert self.hs_joint_corner_dz * 2 < self.root_height
dy = self.hs_joint_corner_dx
dx = self.hs_joint_corner_dz
assert self.root_joint.child_width < self.base_width
assert self.root_joint.child_corner_dx * 2 < self.base_width
assert self.root_joint.child_corner_dz * 2 < self.root_height
dy = self.root_joint.child_corner_dx
dx = self.root_joint.child_corner_dz
holes = [
Hole(x=-dx, y=-dy),
Hole(x=dx, y=-dy),
@ -288,17 +230,17 @@ class WingProfile(Model):
]
return MountingBox(
length=self.root_height,
width=self.base_plate_width,
width=self.root_joint.child_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.hs_joint_corner_hole_diam,
hole_diam=self.root_joint.corner_hole_diam,
centred=(True, True),
flip_y=self.flip,
)
def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.base_plate_width) / 2
base_dy = self.base_joint.joint_height
base_dx = -(self.base_width - self.root_joint.child_width) / 2
base_dy = self.root_joint.hirth_joint.joint_height
loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2)
tags = [
("shoulder",
@ -351,14 +293,6 @@ class WingProfile(Model):
.constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane")
.constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis")
)
hs_joint = self.base_hs_joint()
(
result
.addS(hs_joint, name="hs", role=Role.CHILD, material=self.mat_hs_joint)
.constrain("hs?conn0", "base?conn0", "Plane", param=0)
.constrain("hs?conn1", "base?conn1", "Plane", param=0)
.constrain("hs?conn2", "base?conn2", "Plane", param=0)
)
return result.solve()
@ -627,9 +561,11 @@ class WingProfile(Model):
parts: Optional[list[str]] = None,
shoulder_deflection: float = 0.0,
elbow_wrist_deflection: float = 0.0,
root_offset: int = 5,
fastener_pos: float = 0.0,
) -> Cq.Assembly():
if parts is None:
parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
parts = ["root", "s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
result = (
Cq.Assembly()
)
@ -639,8 +575,23 @@ class WingProfile(Model):
if "s0" in parts:
result.add(self.assembly_s0(), name="s0")
if "root" in parts:
result.addS(self.root_joint.assembly(
offset=root_offset,
fastener_pos=fastener_pos,
), name="root")
result.constrain("root/parent", "Fixed")
if "s0" in parts and "root" in parts:
(
result
.constrain("s0/base?conn0", "root/child?conn0", "Plane", param=0)
.constrain("s0/base?conn1", "root/child?conn1", "Plane", param=0)
.constrain("s0/base?conn2", "root/child?conn2", "Plane", param=0)
)
if "shoulder" in parts:
result.add(self.shoulder_joint.assembly(deflection=shoulder_deflection * 80), name="shoulder")
result.add(self.shoulder_joint.assembly(
fastener_pos=fastener_pos,
deflection=shoulder_deflection * 80), name="shoulder")
if "s0" in parts and "shoulder" in parts:
(
result