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) STRUCTURE = _color('gray', 0.4)
DECORATION = _color('lightseagreen', 0.4) DECORATION = _color('lightseagreen', 0.4)
ELECTRONIC = _color('mediumorchid', 0.5) ELECTRONIC = _color('mediumorchid', 0.5)
MARKER = _color('white', 1.0)
def __init__(self, color: Cq.Color): def __init__(self, color: Cq.Color):
self.color = color self.color = color

View File

@ -37,6 +37,7 @@ from nhf.parts.joints import HirthJoint, TorsionJoint
from nhf.parts.handle import Handle, BayonetMount from nhf.parts.handle import Handle, BayonetMount
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.touhou.houjuu_nue.joints as MJ
import nhf.utils import nhf.utils
@dataclass @dataclass
@ -88,33 +89,21 @@ class Parameters(Model):
hs_joint_axis_cbore_depth: float = 3 hs_joint_axis_cbore_depth: float = 3
wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(
shoulder_height = 80, shoulder_height=100.0,
elbow_height = 100, elbow_height=110.0,
)) ))
# 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 wing_root_wall_thickness: float = 8
shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( shoulder_joint: MJ.ShoulderJoint = field(default_factory=lambda: MJ.ShoulderJoint(
radius_track=18, shoulder_height=100.0,
radius_rider=18, ))
groove_radius_outer=16, elbow_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
groove_radius_inner=13, ))
track_disk_height=5.0, wrist_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
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
shoulder_attach_dist: float = 25
shoulder_attach_diam: float = 8
""" """
Heights for various wing joints, where the numbers start from the first Heights for various wing joints, where the numbers start from the first
@ -325,8 +314,8 @@ class Parameters(Model):
""" """
return MW.wing_root( return MW.wing_root(
joint=self.hs_hirth_joint, joint=self.hs_hirth_joint,
shoulder_attach_dist=self.shoulder_attach_dist, shoulder_attach_dist=self.shoulder_joint.attach_dist,
shoulder_attach_diam=self.shoulder_attach_diam, shoulder_attach_diam=self.shoulder_joint.attach_diam,
wall_thickness=self.wing_root_wall_thickness, wall_thickness=self.wing_root_wall_thickness,
conn_height=self.wing_profile.shoulder_height, conn_height=self.wing_profile.shoulder_height,
conn_thickness=self.wing_s0_thickness, conn_thickness=self.wing_s0_thickness,
@ -334,169 +323,25 @@ class Parameters(Model):
@target(name="wing/proto-shoulder-joint-parent", prototype=True) @target(name="wing/proto-shoulder-joint-parent", prototype=True)
def proto_shoulder_joint_parent(self): 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) @target(name="wing/proto-shoulder-joint-child", prototype=True)
def proto_shoulder_joint_child(self): def proto_shoulder_joint_child(self):
return self.shoulder_torsion_joint.rider() return self.shoulder_joint.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
@assembly() @assembly()
def shoulder_assembly(self) -> Cq.Assembly: def shoulder_assembly(self):
directrix = 0 return self.shoulder_joint.assembly(
result = ( wing_root_wall_thickness=self.wing_root_wall_thickness,
Cq.Assembly() lip_height=self.wing_s1_thickness,
.add(self.shoulder_joint_child(), name="child", hole_dist=self.wing_s1_shoulder_spacer_hole_dist,
color=Role.CHILD.color) spacer_hole_diam=self.wing_s1_spacer_hole_diam,
.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)
) )
TorsionJoint.add_constraints(result, @assembly()
rider="child/rider_top", def elbow_assembly(self):
track="parent_top", return self.elbow_joint.assembly()
spring="spring_top", @assembly()
directrix=directrix) def wrist_assembly(self):
TorsionJoint.add_constraints(result, return self.wrist_joint.assembly()
rider="child/rider_bot",
track="parent_bot",
spring="spring_bot",
directrix=directrix)
return result.solve()
@target(name="wing/s1-spacer", kind=TargetKind.DXF) @target(name="wing/s1-spacer", kind=TargetKind.DXF)
def wing_s1_spacer(self) -> Cq.Workplane: def wing_s1_spacer(self) -> Cq.Workplane:
@ -515,61 +360,93 @@ class Parameters(Model):
@target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF)
def wing_s1_shoulder_spacer(self) -> Cq.Workplane: 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 dx = self.wing_s1_shoulder_spacer_hole_dist
h = self.wing_s1_spacer_thickness 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 = ( result = (
Cq.Workplane('XZ') Cq.Workplane('XY')
.sketch() .sketch()
.rect(self.wing_s1_shoulder_spacer_width, .rect(length, self.wing_s1_thickness)
self.wing_s1_thickness)
.push([ .push([
(0, 0), (0, 0),
(dx, 0), (dx, 0),
]) ])
.circle(self.wing_s1_spacer_hole_diam / 2, mode='s') .circle(hole_diam / 2, mode='s')
.finalize() .finalize()
.extrude(h) .extrude(h)
) )
# Tag the mating surfaces to be glued # Tag the mating surfaces to be glued
result.faces("<Z").workplane().moveTo(0, h).tagPlane("weld1") result.faces("<Y").workplane().moveTo(length / 2, h).tagPlane("left")
result.faces(">Z").workplane().moveTo(0, -h).tagPlane("weld2") result.faces(">Y").workplane().moveTo(-length / 2, h).tagPlane("right")
# Tag the directrix # Tag the directrix
result.faces("<Y").tag("dir") result.faces(">Z").tag("dir")
# Tag the holes # Tag the holes
plane = result.faces(">Y").workplane() plane = result.faces(">Z").workplane()
# Side closer to the parent is 0 # Side closer to the parent is 0
plane.moveTo(-dx, 0).tagPlane("conn0") plane.moveTo(dx, 0).tagPlane("conn0")
plane.tagPlane("conn1") plane.tagPlane("conn1")
return result 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) @target(name="wing/r1s1", kind=TargetKind.DXF)
def wing_r1s1_profile(self) -> Cq.Sketch: def wing_r1s1_profile(self) -> Cq.Sketch:
"""
FIXME: Output individual segment profiles
"""
return self.wing_profile.profile() return self.wing_profile.profile()
def wing_r1s1_panel(self, front=True) -> Cq.Workplane: def wing_r1s1_panel(self, front=True) -> Cq.Workplane:
profile = self.wing_r1s1_profile() return self.wing_profile.surface_s1(
w = self.wing_s1_shoulder_spacer_width / 2 thickness=self.panel_thickness,
h = (self.wing_profile.shoulder_height - self.shoulder_joint_child_height) / 2 shoulder_joint_child_height=self.shoulder_joint.child_height,
anchors = [ front=front,
("shoulder_top", w, h + self.shoulder_joint_child_height), )
("shoulder_bot", w, h), def wing_r1s2_panel(self, front=True) -> Cq.Workplane:
("middle", 50, -20), return self.wing_profile.surface_s2(
("tip", 270, 50), thickness=self.panel_thickness,
] front=front,
result = ( )
Cq.Workplane("XY") def wing_r1s3_panel(self, front=True) -> Cq.Workplane:
.placeSketch(profile) return self.wing_profile.surface_s3(
.extrude(self.panel_thickness) 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() @assembly()
def wing_r1s1_assembly(self) -> Cq.Assembly: def wing_r1s1_assembly(self) -> Cq.Assembly:
@ -582,56 +459,86 @@ class Parameters(Model):
color=self.material_panel.color) color=self.material_panel.color)
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point", .constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
param=self.wing_s1_thickness) 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"]: for t in ["shoulder_bot", "shoulder_top", "elbow_bot", "elbow_top"]:
name = f"{tag}_spacer" is_top = t.endswith("_top")
( is_parent = t.startswith("shoulder")
result self.assembly_insert_shoulder_spacer(
.add(self.wing_s1_spacer(), name=name, result,
color=self.material_bracket.color) self.wing_s1_shoulder_spacer(),
.constrain(f"panel_front?{tag}", f"{tag}_spacer?weld1", "Plane") point_tag=t,
.constrain(f"panel_back?{tag}", f"{tag}_spacer?weld2", "Plane") flipped=is_top == is_parent,
.constrain(f"{name}?dir", "FixedAxis", param=(0, 1, 0)) )
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() return result.solve()
@assembly() @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 = ( result = (
Cq.Assembly() Cq.Assembly()
) )
if "root" in parts: if "s0" in parts:
( (
result result
.add(self.wing_root(), name="root") .add(self.wing_root(), name="s0")
.constrain("root/scaffold", "Fixed") .constrain("s0/scaffold", "Fixed")
) )
if "shoulder" in parts: if "shoulder" in parts:
result.add(self.shoulder_assembly(), name="shoulder") result.add(self.shoulder_assembly(), name="shoulder")
if "root" in parts and "shoulder" in parts: if "s0" in parts and "shoulder" in parts:
( (
result result
.constrain("root/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") .constrain("s0/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane")
.constrain("root/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") .constrain("s0/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane")
.constrain("root/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") .constrain("s0/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane")
.constrain("root/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") .constrain("s0/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane")
) )
if "s1" in parts: if "s1" in parts:
@ -653,6 +560,45 @@ class Parameters(Model):
"s1/shoulder_top_spacer?conn1", "s1/shoulder_top_spacer?conn1",
"Plane") "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() return result.solve()
@assembly() @assembly()

View File

@ -2,12 +2,209 @@ 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
from nhf.build import Model, target from nhf.build import Model, target, assembly
import nhf.parts.springs as springs import nhf.parts.springs as springs
from nhf.parts.joints import TorsionJoint
import nhf.utils import nhf.utils
TOL = 1e-6 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 @dataclass
class Beam: class Beam:
""" """
@ -69,6 +266,7 @@ class Beam:
) )
return result return result
@dataclass @dataclass
class DiskJoint(Model): class DiskJoint(Model):
""" """
@ -325,6 +523,7 @@ class DiskJoint(Model):
) )
return result.solve() return result.solve()
@dataclass @dataclass
class ElbowJoint: class ElbowJoint:
""" """
@ -375,10 +574,10 @@ class ElbowJoint:
) )
return result return result
def parent_joint_bot(self) -> Cq.Workplane: def parent_joint_lower(self) -> Cq.Workplane:
return self.disk_joint.housing_lower() 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)) axial_offset = Cq.Location((self.parent_arm_radius, 0, 0))
housing_dz = self.disk_joint.housing_upper_dz housing_dz = self.disk_joint.housing_upper_dz
conn_h = self.parent_beam.spine_thickness conn_h = self.parent_beam.spine_thickness
@ -396,12 +595,11 @@ class ElbowJoint:
) )
housing = self.disk_joint.housing_upper() housing = self.disk_joint.housing_upper()
result = ( result = (
Cq.Assembly() self.parent_beam.beam()
.add(housing, name="housing", .add(housing, name="housing",
loc=axial_offset * Cq.Location((0, 0, housing_dz))) loc=axial_offset * Cq.Location((0, 0, housing_dz)))
.add(connector, name="connector", .add(connector, name="connector",
loc=axial_offset) loc=axial_offset)
.add(self.parent_beam.beam(), name="beam")
) )
return result return result
@ -410,19 +608,21 @@ class ElbowJoint:
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(self.child_joint(), name="child", color=Role.CHILD.color) .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_lower(), name="parent_lower", color=Role.CASING.color)
.add(self.parent_joint_top(), name="parent_top", color=Role.PARENT.color) .add(self.parent_joint_upper(), name="parent_upper", color=Role.PARENT.color)
.constrain("parent_bot", "Fixed") .constrain("parent_lower", "Fixed")
) )
self.disk_joint.add_constraints( self.disk_joint.add_constraints(
result, result,
housing_lower="parent_bot", housing_lower="parent_lower",
housing_upper="parent_top/housing", housing_upper="parent_upper/housing",
disk="child/disk", disk="child/disk",
angle=(0, 0, angle + da), angle=(0, 0, angle + da),
) )
return result.solve() return result.solve()
if __name__ == '__main__': if __name__ == '__main__':
p = ShoulderJoint()
p.build_all()
p = DiskJoint() p = DiskJoint()
p.build_all() p.build_all()

View File

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

View File

@ -198,7 +198,7 @@ def wing_root(joint: HirthJoint,
for sign in [False, True]: for sign in [False, True]:
y = conn_height / 2 - wall_thickness y = conn_height / 2 - wall_thickness
side = "bottom" if sign else "top" side = "bottom" if sign else "top"
y = y if sign else -y y = -y if sign else y
plane = ( plane = (
result result
# Create connector holes # Create connector holes
@ -211,7 +211,7 @@ def wing_root(joint: HirthJoint,
side = "bot" side = "bot"
for i, (px, py) in enumerate(attach_points): for i, (px, py) in enumerate(attach_points):
tag = f"conn_{side}{i}" 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("<Z").tag("base")
result.faces(">X").tag("conn") result.faces(">X").tag("conn")
@ -430,7 +430,7 @@ class WingProfile:
self.elbow_angle + 90), self.elbow_angle + 90),
("elbow_top", ("elbow_top",
self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height), 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 h = (self.wrist_height - wrist_joint_parent_height) / 2
tags_wrist = [ tags_wrist = [
@ -439,7 +439,7 @@ class WingProfile:
self.wrist_angle + 90), self.wrist_angle + 90),
("wrist_top", ("wrist_top",
self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height), self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height),
self.wrist_angle + 270), self.wrist_angle - 90),
] ]
profile = self.profile_s2() profile = self.profile_s2()
tags = tags_elbow + tags_wrist tags = tags_elbow + tags_wrist
@ -465,7 +465,7 @@ class WingProfile:
self.elbow_angle + 90), self.elbow_angle + 90),
("wrist_top", ("wrist_top",
self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height),
self.elbow_angle + 270), self.elbow_angle - 90),
] ]
profile = self.profile_s3() profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) 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 math
import cadquery as Cq import cadquery as Cq
from nhf import Role
from typing import Union, Tuple from typing import Union, Tuple
@ -19,7 +20,6 @@ def tagPoint(self, tag: str):
Cq.Workplane.tagPoint = tagPoint Cq.Workplane.tagPoint = tagPoint
def tagPlane(self, tag: str, def tagPlane(self, tag: str,
direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'): direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'):
""" """
@ -52,6 +52,57 @@ def tagPlane(self, tag: str,
Cq.Workplane.tagPlane = tagPlane 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, def extrude_with_markers(sketch: Cq.Sketch,
thickness: float, thickness: float,
tags: list[Tuple[str, Tuple[float, float], 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() plane = result.faces("<Z" if reverse else ">Z").workplane()
sign = -1 if reverse else 1 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) theta = sign * math.radians(angle)
direction = (math.cos(theta), math.sin(theta), 0) direction = (math.cos(theta), math.sin(theta), 0)
plane.moveTo(px, sign * py).tagPlane(tag) plane.moveTo(px, sign * py).tagPlane(tag)