cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
6 changed files with 469 additions and 267 deletions
Showing only changes of commit 641755314e - Show all commits

View File

@ -21,6 +21,7 @@ class Role(Enum):
STRUCTURE = _color('gray', 0.4)
DECORATION = _color('lightseagreen', 0.4)
ELECTRONIC = _color('mediumorchid', 0.5)
MARKER = _color('white', 1.0)
def __init__(self, color: Cq.Color):
self.color = color

View File

@ -37,6 +37,7 @@ from nhf.parts.joints import HirthJoint, TorsionJoint
from nhf.parts.handle import Handle, BayonetMount
import nhf.touhou.houjuu_nue.wing as MW
import nhf.touhou.houjuu_nue.trident as MT
import nhf.touhou.houjuu_nue.joints as MJ
import nhf.utils
@dataclass
@ -88,33 +89,21 @@ class Parameters(Model):
hs_joint_axis_cbore_depth: float = 3
wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(
shoulder_height = 80,
elbow_height = 100,
shoulder_height=100.0,
elbow_height=110.0,
))
# Exterior radius of the wing root assembly
wing_root_radius: float = 40
wing_root_wall_thickness: float = 8
shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=18,
radius_rider=18,
groove_radius_outer=16,
groove_radius_inner=13,
track_disk_height=5.0,
rider_disk_height=5.0,
# M8 Axle
radius_axle=3.0,
# inner diameter = 9
radius_spring=9/2 + 1.2,
spring_thickness=1.3,
spring_height=7.5,
shoulder_joint: MJ.ShoulderJoint = field(default_factory=lambda: MJ.ShoulderJoint(
shoulder_height=100.0,
))
elbow_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
))
wrist_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
))
# 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
@ -325,8 +314,8 @@ class Parameters(Model):
"""
return MW.wing_root(
joint=self.hs_hirth_joint,
shoulder_attach_dist=self.shoulder_attach_dist,
shoulder_attach_diam=self.shoulder_attach_diam,
shoulder_attach_dist=self.shoulder_joint.attach_dist,
shoulder_attach_diam=self.shoulder_joint.attach_diam,
wall_thickness=self.wing_root_wall_thickness,
conn_height=self.wing_profile.shoulder_height,
conn_thickness=self.wing_s0_thickness,
@ -334,169 +323,25 @@ class Parameters(Model):
@target(name="wing/proto-shoulder-joint-parent", prototype=True)
def proto_shoulder_joint_parent(self):
return self.shoulder_torsion_joint.track()
return self.shoulder_joint.torsion_joint.track()
@target(name="wing/proto-shoulder-joint-child", prototype=True)
def proto_shoulder_joint_child(self):
return self.shoulder_torsion_joint.rider()
@target(name="wing/shoulder_joint_parent")
def shoulder_joint_parent(self) -> Cq.Workplane:
joint = self.shoulder_torsion_joint
# Thickness of the lip connecting this joint to the wing root
lip_thickness = 10
lip_width = 25
lip_guard_ext = 40
lip_guard_height = self.wing_root_wall_thickness + lip_thickness
assert lip_guard_ext > joint.radius_track
lip_guard = (
Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height)
.located(Cq.Location((0, -lip_width/2 , 0)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height))
)
result = (
joint.track()
.union(lip_guard, tol=1e-6)
# Extrude the handle
.copyWorkplane(Cq.Workplane(
'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness))))
.rect(lip_width, lip_thickness, centered=(True, False))
.extrude("next")
# Connector holes on the lip
.copyWorkplane(Cq.Workplane(
'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)
)
result.moveTo(0, 0).tagPlane('conn0')
result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1')
return result
@property
def shoulder_joint_child_height(self) -> float:
"""
Calculates the y distance between two joint surfaces on the child side
of the shoulder joint.
"""
joint = self.shoulder_torsion_joint
return self.wing_profile.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height
@target(name="wing/shoulder_joint_child")
def shoulder_joint_child(self) -> Cq.Assembly:
"""
Creates the top/bottom shoulder child joint
"""
joint = self.shoulder_torsion_joint
# Half of the height of the bridging cylinder
dh = self.wing_profile.shoulder_height / 2 - joint.total_height
core_start_angle = 30
core_end_angle1 = 90
core_end_angle2 = 180
core_thickness = 2
core_profile1 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle)
.segment((0, 0))
.close()
.assemble()
.circle(joint.radius_rider - core_thickness, mode='s')
)
core_profile2 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle))
.segment((0, 0))
.close()
.assemble()
.circle(joint.radius_rider - core_thickness, mode='s')
)
core = (
Cq.Workplane('XY')
.placeSketch(core_profile1)
.toPending()
.extrude(dh * 2)
.copyWorkplane(Cq.Workplane('XY'))
.placeSketch(core_profile2)
.toPending()
.extrude(dh * 2)
.translate(Cq.Vector(0, 0, -dh))
)
# Create the upper and lower lips
lip_height = self.wing_s1_thickness
lip_thickness = joint.rider_disk_height
lip_ext = 40 + joint.radius_rider
hole_dx = self.wing_s1_shoulder_spacer_hole_dist
assert lip_height / 2 <= joint.radius_rider
lip = (
Cq.Workplane('XY')
.box(lip_ext, lip_height, lip_thickness,
centered=(False, True, False))
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(radius=joint.radius_rider, height=lip_thickness,
centered=(True, True, False),
combine='cut')
.faces(">Z")
.workplane()
)
hole_x = lip_ext - hole_dx / 2
for i in range(2):
x = hole_x - i * hole_dx
lip = lip.moveTo(x, 0).hole(self.wing_s1_spacer_hole_diam)
for i in range(2):
x = hole_x - i * hole_dx
(
lip
.moveTo(x, 0)
.tagPlane(f"conn{1 - i}")
)
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
result = (
Cq.Assembly()
.add(core, name="core", loc=Cq.Location())
.add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top",
loc=Cq.Location((0, 0, dh), (0, 0, 1), -90))
.add(joint.rider(rider_slot_begin=180), name="rider_bot",
loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate)
.add(lip, name="lip_top",
loc=Cq.Location((0, 0, dh)))
.add(lip, name="lip_bot",
loc=Cq.Location((0, 0, -dh)) * loc_rotate)
)
return result
return self.shoulder_joint.torsion_joint.rider()
@assembly()
def shoulder_assembly(self) -> Cq.Assembly:
directrix = 0
result = (
Cq.Assembly()
.add(self.shoulder_joint_child(), name="child",
color=Role.CHILD.color)
.constrain("child/core", "Fixed")
.add(self.shoulder_torsion_joint.spring(), name="spring_top",
color=Role.DAMPING.color)
.add(self.shoulder_joint_parent(), name="parent_top",
color=Role.PARENT.color)
.add(self.shoulder_torsion_joint.spring(), name="spring_bot",
color=Role.DAMPING.color)
.add(self.shoulder_joint_parent(), name="parent_bot",
color=Role.PARENT.color)
def shoulder_assembly(self):
return self.shoulder_joint.assembly(
wing_root_wall_thickness=self.wing_root_wall_thickness,
lip_height=self.wing_s1_thickness,
hole_dist=self.wing_s1_shoulder_spacer_hole_dist,
spacer_hole_diam=self.wing_s1_spacer_hole_diam,
)
TorsionJoint.add_constraints(result,
rider="child/rider_top",
track="parent_top",
spring="spring_top",
directrix=directrix)
TorsionJoint.add_constraints(result,
rider="child/rider_bot",
track="parent_bot",
spring="spring_bot",
directrix=directrix)
return result.solve()
@assembly()
def elbow_assembly(self):
return self.elbow_joint.assembly()
@assembly()
def wrist_assembly(self):
return self.wrist_joint.assembly()
@target(name="wing/s1-spacer", kind=TargetKind.DXF)
def wing_s1_spacer(self) -> Cq.Workplane:
@ -515,61 +360,93 @@ class Parameters(Model):
@target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF)
def wing_s1_shoulder_spacer(self) -> Cq.Workplane:
"""
The mate tags are on the side closer to the holes.
Creates a rectangular spacer. This could be cut from acrylic.
There are two holes on the top of the spacer. With the holes
"""
dx = self.wing_s1_shoulder_spacer_hole_dist
h = self.wing_s1_spacer_thickness
length = self.wing_s1_shoulder_spacer_width
hole_diam = self.wing_s1_spacer_hole_diam
assert dx + hole_diam < length / 2
result = (
Cq.Workplane('XZ')
Cq.Workplane('XY')
.sketch()
.rect(self.wing_s1_shoulder_spacer_width,
self.wing_s1_thickness)
.rect(length, self.wing_s1_thickness)
.push([
(0, 0),
(dx, 0),
])
.circle(self.wing_s1_spacer_hole_diam / 2, mode='s')
.circle(hole_diam / 2, mode='s')
.finalize()
.extrude(h)
)
# Tag the mating surfaces to be glued
result.faces("<Z").workplane().moveTo(0, h).tagPlane("weld1")
result.faces(">Z").workplane().moveTo(0, -h).tagPlane("weld2")
result.faces("<Y").workplane().moveTo(length / 2, h).tagPlane("left")
result.faces(">Y").workplane().moveTo(-length / 2, h).tagPlane("right")
# Tag the directrix
result.faces("<Y").tag("dir")
result.faces(">Z").tag("dir")
# Tag the holes
plane = result.faces(">Y").workplane()
plane = result.faces(">Z").workplane()
# Side closer to the parent is 0
plane.moveTo(-dx, 0).tagPlane("conn0")
plane.moveTo(dx, 0).tagPlane("conn0")
plane.tagPlane("conn1")
return result
def assembly_insert_shoulder_spacer(
self,
assembly,
spacer,
point_tag: str,
front_tag: str = "panel_front",
back_tag: str = "panel_back",
flipped: bool = False,
):
"""
For a child joint facing up, front panel should be on the right, back
panel on the left
"""
site_front, site_back = "right", "left"
if flipped:
site_front, site_back = site_back, site_front
angle = 0
(
assembly
.add(spacer,
name=f"{point_tag}_spacer",
color=self.material_bracket.color)
.constrain(f"{front_tag}?{point_tag}",
f"{point_tag}_spacer?{site_front}", "Plane")
.constrain(f"{back_tag}?{point_tag}",
f"{point_tag}_spacer?{site_back}", "Plane")
.constrain(f"{point_tag}_spacer?dir", f"{front_tag}?{point_tag}_dir",
"Axis", param=angle)
)
@target(name="wing/r1s1", kind=TargetKind.DXF)
def wing_r1s1_profile(self) -> Cq.Sketch:
"""
FIXME: Output individual segment profiles
"""
return self.wing_profile.profile()
def wing_r1s1_panel(self, front=True) -> Cq.Workplane:
profile = self.wing_r1s1_profile()
w = self.wing_s1_shoulder_spacer_width / 2
h = (self.wing_profile.shoulder_height - self.shoulder_joint_child_height) / 2
anchors = [
("shoulder_top", w, h + self.shoulder_joint_child_height),
("shoulder_bot", w, h),
("middle", 50, -20),
("tip", 270, 50),
]
result = (
Cq.Workplane("XY")
.placeSketch(profile)
.extrude(self.panel_thickness)
return self.wing_profile.surface_s1(
thickness=self.panel_thickness,
shoulder_joint_child_height=self.shoulder_joint.child_height,
front=front,
)
def wing_r1s2_panel(self, front=True) -> Cq.Workplane:
return self.wing_profile.surface_s2(
thickness=self.panel_thickness,
front=front,
)
def wing_r1s3_panel(self, front=True) -> Cq.Workplane:
return self.wing_profile.surface_s3(
thickness=self.panel_thickness,
front=front,
)
plane = result.faces(">Z" if front else "<Z").workplane()
sign = 1 if front else -1
for name, px, py in anchors:
plane.moveTo(px, sign * py).tagPlane(name)
return result
@assembly()
def wing_r1s1_assembly(self) -> Cq.Assembly:
@ -582,56 +459,86 @@ class Parameters(Model):
color=self.material_panel.color)
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
param=self.wing_s1_thickness)
.add(self.wing_s1_shoulder_spacer(),
name="shoulder_bot_spacer",
color=self.material_bracket.color)
.constrain("panel_front?shoulder_bot", "shoulder_bot_spacer?weld1", "Plane")
.constrain("panel_back?shoulder_bot", "shoulder_bot_spacer?weld2", "Plane")
.constrain("shoulder_bot_spacer?dir", "FixedAxis", param=(0, 1, 0))
.add(self.wing_s1_shoulder_spacer(),
name="shoulder_top_spacer",
color=self.material_bracket.color)
.constrain("panel_front?shoulder_top", "shoulder_top_spacer?weld2", "Plane")
.constrain("panel_back?shoulder_top", "shoulder_top_spacer?weld1", "Plane")
.constrain("shoulder_top_spacer?dir", "FixedAxis", param=(0, -1, 0))
# Should be controlled by point value directly
#.constrain("shoulder_bot_spacer?dir", "shoulder_top_spacer?dir", "Point",
# self.shoulder_joint_child_height)
)
for tag in ["middle", "tip"]:
name = f"{tag}_spacer"
(
result
.add(self.wing_s1_spacer(), name=name,
color=self.material_bracket.color)
.constrain(f"panel_front?{tag}", f"{tag}_spacer?weld1", "Plane")
.constrain(f"panel_back?{tag}", f"{tag}_spacer?weld2", "Plane")
.constrain(f"{name}?dir", "FixedAxis", param=(0, 1, 0))
for t in ["shoulder_bot", "shoulder_top", "elbow_bot", "elbow_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("shoulder")
self.assembly_insert_shoulder_spacer(
result,
self.wing_s1_shoulder_spacer(),
point_tag=t,
flipped=is_top == is_parent,
)
return result.solve()
@assembly()
def wing_r1s2_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.wing_r1s2_panel(front=True), name="panel_front",
color=self.material_panel.color)
.constrain("panel_front", "Fixed")
.add(self.wing_r1s2_panel(front=False), name="panel_back",
color=self.material_panel.color)
# FIXME: Use s2 thickness
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
param=self.wing_s1_thickness)
)
for t in ["elbow_bot", "elbow_top", "wrist_bot", "wrist_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("elbow")
self.assembly_insert_shoulder_spacer(
result,
self.wing_s1_shoulder_spacer(),
point_tag=t,
flipped=is_top == is_parent,
)
return result.solve()
@assembly()
def wing_r1s3_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.wing_r1s3_panel(front=True), name="panel_front",
color=self.material_panel.color)
.constrain("panel_front", "Fixed")
.add(self.wing_r1s3_panel(front=False), name="panel_back",
color=self.material_panel.color)
# FIXME: Use s2 thickness
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
param=self.wing_s1_thickness)
)
for t in ["wrist_bot", "wrist_top"]:
self.assembly_insert_shoulder_spacer(
result,
self.wing_s1_shoulder_spacer(),
point_tag=t
)
return result.solve()
@assembly()
def wing_r1_assembly(self, parts=["root", "shoulder", "s1"]) -> Cq.Assembly:
def wing_r1_assembly(
self,
parts=["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"],
) -> Cq.Assembly:
result = (
Cq.Assembly()
)
if "root" in parts:
if "s0" in parts:
(
result
.add(self.wing_root(), name="root")
.constrain("root/scaffold", "Fixed")
.add(self.wing_root(), name="s0")
.constrain("s0/scaffold", "Fixed")
)
if "shoulder" in parts:
result.add(self.shoulder_assembly(), name="shoulder")
if "root" in parts and "shoulder" in parts:
if "s0" in parts and "shoulder" in parts:
(
result
.constrain("root/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane")
.constrain("root/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane")
.constrain("root/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane")
.constrain("root/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane")
.constrain("s0/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane")
.constrain("s0/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane")
.constrain("s0/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane")
.constrain("s0/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane")
)
if "s1" in parts:
@ -653,6 +560,45 @@ class Parameters(Model):
"s1/shoulder_top_spacer?conn1",
"Plane")
)
if "elbow" in parts:
result.add(self.elbow_assembly(), name="elbow")
if "s2" in parts:
result.add(self.wing_r1s2_assembly(), name="s2")
if "s1" in parts and "elbow" in parts:
(
result
.constrain("elbow/parent_upper/top?conn1",
"s1/elbow_top_spacer?conn1",
"Plane")
.constrain("elbow/parent_upper/top?conn0",
"s1/elbow_top_spacer?conn0",
"Plane")
.constrain("elbow/parent_upper/bot?conn1",
"s1/elbow_bot_spacer?conn1",
"Plane")
.constrain("elbow/parent_upper/bot?conn0",
"s1/elbow_bot_spacer?conn0",
"Plane")
)
if "s2" in parts and "elbow" in parts:
(
result
.constrain("elbow/child/bot?conn0",
"s2/elbow_bot_spacer?conn0",
"Plane")
.constrain("elbow/child/bot?conn1",
"s2/elbow_bot_spacer?conn1",
"Plane")
.constrain("elbow/child/top?conn0",
"s2/elbow_top_spacer?conn0",
"Plane")
.constrain("elbow/child/top?conn1",
"s2/elbow_top_spacer?conn1",
"Plane")
)
return result.solve()
@assembly()

View File

@ -2,12 +2,209 @@ from dataclasses import dataclass, field
from typing import Optional, Tuple
import cadquery as Cq
from nhf import Role
from nhf.build import Model, target
from nhf.build import Model, target, assembly
import nhf.parts.springs as springs
from nhf.parts.joints import TorsionJoint
import nhf.utils
TOL = 1e-6
@dataclass
class ShoulderJoint(Model):
shoulder_height: float = 100.0
torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=18,
radius_rider=18,
groove_radius_outer=16,
groove_radius_inner=13,
track_disk_height=5.0,
rider_disk_height=5.0,
# M8 Axle
radius_axle=3.0,
# inner diameter = 9
radius_spring=9/2 + 1.2,
spring_thickness=1.3,
spring_height=7.5,
))
# Two holes on each side (top and bottom) are used to attach the shoulder
# joint. This governs the distance between these two holes
attach_dist: float = 25
attach_diam: float = 8
@target(name="shoulder-joint/parent")
def parent(self,
wing_root_wall_thickness: float = 5.0) -> Cq.Workplane:
joint = self.torsion_joint
# Thickness of the lip connecting this joint to the wing root
lip_thickness = 10
lip_width = 25
lip_guard_ext = 40
lip_guard_height = wing_root_wall_thickness + lip_thickness
assert lip_guard_ext > joint.radius_track
lip_guard = (
Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height)
.located(Cq.Location((0, -lip_width/2 , 0)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height))
)
result = (
joint.track()
.union(lip_guard, tol=1e-6)
# Extrude the handle
.copyWorkplane(Cq.Workplane(
'YZ', origin=Cq.Vector((88, 0, wing_root_wall_thickness))))
.rect(lip_width, lip_thickness, centered=(True, False))
.extrude("next")
# Connector holes on the lip
.copyWorkplane(Cq.Workplane(
'YX', origin=Cq.Vector((57, 0, wing_root_wall_thickness))))
.hole(self.attach_diam)
.moveTo(0, self.attach_dist)
.hole(self.attach_diam)
)
result.moveTo(0, 0).tagPlane('conn0')
result.moveTo(0, self.attach_dist).tagPlane('conn1')
return result
@property
def child_height(self) -> float:
"""
Calculates the y distance between two joint surfaces on the child side
of the shoulder joint.
"""
joint = self.torsion_joint
return self.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height
@target(name="shoulder-joint/child")
def child(self,
lip_height: float = 20.0,
hole_dist: float = 10.0,
spacer_hole_diam: float = 8.0) -> Cq.Assembly:
"""
Creates the top/bottom shoulder child joint
"""
joint = self.torsion_joint
# Half of the height of the bridging cylinder
dh = self.shoulder_height / 2 - joint.total_height
core_start_angle = 30
core_end_angle1 = 90
core_end_angle2 = 180
core_thickness = 2
core_profile1 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle)
.segment((0, 0))
.close()
.assemble()
.circle(joint.radius_rider - core_thickness, mode='s')
)
core_profile2 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle))
.segment((0, 0))
.close()
.assemble()
.circle(joint.radius_rider - core_thickness, mode='s')
)
core = (
Cq.Workplane('XY')
.placeSketch(core_profile1)
.toPending()
.extrude(dh * 2)
.copyWorkplane(Cq.Workplane('XY'))
.placeSketch(core_profile2)
.toPending()
.extrude(dh * 2)
.translate(Cq.Vector(0, 0, -dh))
)
# Create the upper and lower lips
lip_thickness = joint.rider_disk_height
lip_ext = 40 + joint.radius_rider
assert lip_height / 2 <= joint.radius_rider
lip = (
Cq.Workplane('XY')
.box(lip_ext, lip_height, lip_thickness,
centered=(False, True, False))
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(radius=joint.radius_rider, height=lip_thickness,
centered=(True, True, False),
combine='cut')
.faces(">Z")
.workplane()
)
hole_x = lip_ext - hole_dist / 2
for i in range(2):
x = hole_x - i * hole_dist
lip = lip.moveTo(x, 0).hole(spacer_hole_diam)
for i in range(2):
x = hole_x - i * hole_dist
(
lip
.moveTo(x, 0)
.tagPlane(f"conn{1 - i}")
)
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
result = (
Cq.Assembly()
.add(core, name="core", loc=Cq.Location())
.add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top",
loc=Cq.Location((0, 0, dh), (0, 0, 1), -90))
.add(joint.rider(rider_slot_begin=180), name="rider_bot",
loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate)
.add(lip, name="lip_top",
loc=Cq.Location((0, 0, dh)))
.add(lip, name="lip_bot",
loc=Cq.Location((0, 0, -dh)) * loc_rotate)
)
return result
@assembly()
def assembly(self,
wing_root_wall_thickness: float = 5.0,
lip_height: float = 5.0,
hole_dist: float = 10.0,
spacer_hole_diam: float = 8.0
) -> Cq.Assembly:
directrix = 0
result = (
Cq.Assembly()
.add(self.child(lip_height=lip_height,
hole_dist=hole_dist,
spacer_hole_diam=spacer_hole_diam),
name="child",
color=Role.CHILD.color)
.constrain("child/core", "Fixed")
.add(self.torsion_joint.spring(), name="spring_top",
color=Role.DAMPING.color)
.add(self.parent(wing_root_wall_thickness),
name="parent_top",
color=Role.PARENT.color)
.add(self.torsion_joint.spring(), name="spring_bot",
color=Role.DAMPING.color)
.add(self.parent(wing_root_wall_thickness),
name="parent_bot",
color=Role.PARENT.color)
)
TorsionJoint.add_constraints(result,
rider="child/rider_top",
track="parent_top",
spring="spring_top",
directrix=directrix)
TorsionJoint.add_constraints(result,
rider="child/rider_bot",
track="parent_bot",
spring="spring_bot",
directrix=directrix)
return result.solve()
@dataclass
class Beam:
"""
@ -69,6 +266,7 @@ class Beam:
)
return result
@dataclass
class DiskJoint(Model):
"""
@ -325,6 +523,7 @@ class DiskJoint(Model):
)
return result.solve()
@dataclass
class ElbowJoint:
"""
@ -375,10 +574,10 @@ class ElbowJoint:
)
return result
def parent_joint_bot(self) -> Cq.Workplane:
def parent_joint_lower(self) -> Cq.Workplane:
return self.disk_joint.housing_lower()
def parent_joint_top(self):
def parent_joint_upper(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
@ -396,12 +595,11 @@ class ElbowJoint:
)
housing = self.disk_joint.housing_upper()
result = (
Cq.Assembly()
self.parent_beam.beam()
.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
@ -410,19 +608,21 @@ class ElbowJoint:
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")
.add(self.parent_joint_lower(), name="parent_lower", color=Role.CASING.color)
.add(self.parent_joint_upper(), name="parent_upper", color=Role.PARENT.color)
.constrain("parent_lower", "Fixed")
)
self.disk_joint.add_constraints(
result,
housing_lower="parent_bot",
housing_upper="parent_top/housing",
housing_lower="parent_lower",
housing_upper="parent_upper/housing",
disk="child/disk",
angle=(0, 0, angle + da),
)
return result.solve()
if __name__ == '__main__':
p = ShoulderJoint()
p.build_all()
p = DiskJoint()
p.build_all()

View File

@ -1,21 +1,25 @@
import unittest
import cadquery as Cq
import nhf.touhou.houjuu_nue as M
import nhf.touhou.houjuu_nue.parts as MP
import nhf.touhou.houjuu_nue.joints as MJ
from nhf.checks import pairwise_intersection
class TestDiskJoint(unittest.TestCase):
class TestJoints(unittest.TestCase):
def test_collision_0(self):
j = MP.DiskJoint()
def test_shoulder_collision_0(self):
j = MJ.ShoulderJoint()
assembly = j.assembly()
self.assertEqual(pairwise_intersection(assembly), [])
def test_disk_collision_0(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=0)
self.assertEqual(pairwise_intersection(assembly), [])
def test_collision_mid(self):
j = MP.DiskJoint()
def test_disk_collision_mid(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=j.movement_angle / 2)
self.assertEqual(pairwise_intersection(assembly), [])
def test_collision_max(self):
j = MP.DiskJoint()
def test_disk_collision_max(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=j.movement_angle)
self.assertEqual(pairwise_intersection(assembly), [])

View File

@ -198,7 +198,7 @@ def wing_root(joint: HirthJoint,
for sign in [False, True]:
y = conn_height / 2 - wall_thickness
side = "bottom" if sign else "top"
y = y if sign else -y
y = -y if sign else y
plane = (
result
# Create connector holes
@ -211,7 +211,7 @@ def wing_root(joint: HirthJoint,
side = "bot"
for i, (px, py) in enumerate(attach_points):
tag = f"conn_{side}{i}"
plane.moveTo(px, -py if side == "top" else py).tagPlane(tag)
plane.moveTo(px, -py if side == "top" else py).tagPlane(tag, "-Z")
result.faces("<Z").tag("base")
result.faces(">X").tag("conn")
@ -430,7 +430,7 @@ class WingProfile:
self.elbow_angle + 90),
("elbow_top",
self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height),
self.elbow_angle + 270),
self.elbow_angle - 90),
]
h = (self.wrist_height - wrist_joint_parent_height) / 2
tags_wrist = [
@ -439,7 +439,7 @@ class WingProfile:
self.wrist_angle + 90),
("wrist_top",
self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height),
self.wrist_angle + 270),
self.wrist_angle - 90),
]
profile = self.profile_s2()
tags = tags_elbow + tags_wrist
@ -465,7 +465,7 @@ class WingProfile:
self.elbow_angle + 90),
("wrist_top",
self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height),
self.elbow_angle + 270),
self.elbow_angle - 90),
]
profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)

View File

@ -7,6 +7,7 @@ Adds the functions to `Cq.Workplane`:
"""
import math
import cadquery as Cq
from nhf import Role
from typing import Union, Tuple
@ -19,7 +20,6 @@ def tagPoint(self, tag: str):
Cq.Workplane.tagPoint = tagPoint
def tagPlane(self, tag: str,
direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'):
"""
@ -52,6 +52,57 @@ def tagPlane(self, tag: str,
Cq.Workplane.tagPlane = tagPlane
def make_sphere(r: float = 2) -> Cq.Solid:
"""
Makes a full sphere. The default function makes a hemisphere
"""
return Cq.Solid.makeSphere(r, angleDegrees1=-90)
def make_arrow(size: float = 2) -> Cq.Workplane:
cone = Cq.Solid.makeCone(
radius1 = size,
radius2 = 0,
height=size)
result = (
Cq.Workplane("XY")
.cylinder(radius=size / 2, height=size, centered=(True, True, False))
.union(cone.located(Cq.Location((0, 0, size))))
)
result.faces("<Z").tag("dir_rev")
return result
def mark_point(self: Cq.Assembly,
tag: str,
size: float = 2,
color: Cq.Color = Role.MARKER.color) -> Cq.Assembly:
"""
Adds a marker to make a point visible
"""
name = f"{tag}_marker"
return (
self
.add(make_sphere(size), name=name, color=color)
.constrain(tag, name, "Point")
)
Cq.Assembly.markPoint = mark_point
def mark_plane(self: Cq.Assembly,
tag: str,
size: float = 2,
color: Cq.Color = Role.MARKER.color) -> Cq.Assembly:
"""
Adds a marker to make a plane visible
"""
name = tag.replace("?", "__") + "_marker"
return (
self
.add(make_arrow(size), name=name, color=color)
.constrain(tag, f"{name}?dir_rev", "Plane", param=180)
)
Cq.Assembly.markPlane = mark_plane
def extrude_with_markers(sketch: Cq.Sketch,
thickness: float,
tags: list[Tuple[str, Tuple[float, float], float]],
@ -73,7 +124,7 @@ def extrude_with_markers(sketch: Cq.Sketch,
)
plane = result.faces("<Z" if reverse else ">Z").workplane()
sign = -1 if reverse else 1
for tag, (px, py), angle in tag:
for tag, (px, py), angle in tags:
theta = sign * math.radians(angle)
direction = (math.cos(theta), math.sin(theta), 0)
plane.moveTo(px, sign * py).tagPlane(tag)