cosplay: Touhou/Houjuu Nue #4
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
|
@ -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), [])
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
55
nhf/utils.py
55
nhf/utils.py
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue