feat: Anti-collision shoulder joint

This commit is contained in:
Leni Aniva 2024-07-23 16:49:25 -07:00
parent be118be6cc
commit ac509a1625
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
4 changed files with 129 additions and 54 deletions

View File

@ -58,6 +58,8 @@ class MountingBox(Model):
# Generate tags on the opposite side
generate_reverse_tags: bool = False
centre_bot_top_tags: bool = False
# Determines the position of side tags
flip_y: bool = False
@ -105,8 +107,12 @@ class MountingBox(Model):
result.faces(">Y").workplane(origin=result.vertices("<X and >Y and >Z").val().Center()).tagPlane("right")
c_y = ">Y" if self.flip_y else "<Y"
result.faces("<X").workplane(origin=result.vertices(f"<X and {c_y} and >Z").val().Center()).tagPlane("bot")
result.faces(">X").workplane(origin=result.vertices(f">X and {c_y} and >Z").val().Center()).tagPlane("top")
if self.centre_bot_top_tags:
result.faces("<X").workplane(origin=result.edges(f"<X and >Z").val().Center()).tagPlane("bot")
result.faces(">X").workplane(origin=result.edges(f">X and >Z").val().Center()).tagPlane("top")
else:
result.faces("<X").workplane(origin=result.vertices(f"<X and {c_y} and >Z").val().Center()).tagPlane("bot")
result.faces(">X").workplane(origin=result.vertices(f">X and {c_y} and >Z").val().Center()).tagPlane("top")
result.faces(">Z").tag("dir")
return result

View File

@ -41,6 +41,7 @@ import nhf.touhou.houjuu_nue.harness as MH
from nhf.parts.item import Item
import nhf.utils
WING_DEFLECT = 10.0
@dataclass
class Parameters(Model):
"""
@ -51,7 +52,7 @@ class Parameters(Model):
wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r1",
shoulder_angle_bias = 15.0,
shoulder_angle_bias = WING_DEFLECT,
s0_top_hole=False,
s0_bot_hole=True,
))
@ -62,7 +63,7 @@ class Parameters(Model):
))
wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r3",
shoulder_angle_bias = 15.0,
shoulder_angle_bias = WING_DEFLECT,
s0_top_hole=True,
s0_bot_hole=False,
))
@ -75,7 +76,7 @@ class Parameters(Model):
wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l2",
wrist_angle=-30.0,
shoulder_angle_bias = 15.0,
shoulder_angle_bias = WING_DEFLECT,
s0_top_hole=True,
s0_bot_hole=True,
))

View File

@ -261,18 +261,23 @@ class ShoulderJoint(Model):
# On the parent side, drill vertical holes
parent_conn_hole_diam: float = 6.0
# Position of the holes relative
parent_conn_hole_diam: float = 4.0
# Position of the holes relative centre line
parent_conn_hole_pos: list[Tuple[float, float]] = field(default_factory=lambda: [
(15, 8),
(15, -8),
(20, 8),
(20, -8),
])
# Distance from centre of lips to the axis
parent_lip_ext: float = 40.0
parent_lip_length: float = 25.0
parent_lip_width: float = 30.0
parent_lip_thickness: float = 5.0
parent_lip_ext: float = 40.0
parent_lip_guard_height: float = 8.0
# The parent side has arms which connect to the lips
parent_arm_width: float = 25.0
parent_arm_height: float = 12.0
# Generates a child guard which covers up the internals. The lip length is
# relative to the +X surface of the guard.
@ -281,19 +286,19 @@ class ShoulderJoint(Model):
# guard length measured from axle
child_lip_length: float = 40.0
child_lip_width: float = 20.0
child_conn_hole_diam: float = 6.0
child_conn_hole_diam: float = 4.0
# Measured from centre of axle
child_conn_hole_pos: list[float] = field(default_factory=lambda: [15, 25])
child_conn_hole_pos: list[float] = field(default_factory=lambda: [8, 19, 30])
child_core_thickness: float = 3.0
# Rotates the torsion joint to avoid collisions or for some other purpose
axis_rotate_bot: float = 225.0
axis_rotate_top: float = -225.0
axis_rotate_bot: float = 90
axis_rotate_top: float = 0
directrix_id: int = 0
angle_neutral: float = 10.0
angle_max_deflection: float = 80.0
angle_neutral: float = -15.0
angle_max_deflection: float = 90.0
def __post_init__(self):
assert self.parent_lip_length * 2 < self.height
@ -302,51 +307,105 @@ class ShoulderJoint(Model):
def radius(self):
return self.torsion_joint.radius
def parent_arm_loc(self) -> Cq.Location:
def parent_lip_loc(self, left: bool=True) -> Cq.Location:
"""
2d location of the arm surface on the parent side, relative to axle
"""
return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.parent_lip_ext, 0, 0)
dy = self.parent_arm_width / 2
sign = 1 if left else -1
loc_dir = Cq.Location((0,sign * dy,0), (0, 0, 1), sign * 90)
return Cq.Location.from2d(self.parent_lip_ext, 0, 0) * loc_dir
@property
def _max_contraction_angle(self) -> float:
return self.angle_max_deflection + self.angle_neutral
def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid:
"""
Generates a cylindrical sector which cuts away overlapping regions of the child and parent
"""
aspect = self.child_guard_width / self.parent_arm_width
theta = math.radians(self._max_contraction_angle)
theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect))
angle = math.degrees(theta_p)
assert 0 <= angle <= 90
# outer radius of the cut, overestimated
cut_radius = math.sqrt(self.child_guard_width ** 2 + self.parent_arm_width ** 2)
span = 180
result = (
Cq.Solid.makeCylinder(
height=self.height,
radius=cut_radius,
angleDegrees=span,
).cut(Cq.Solid.makeCylinder(
height=self.height,
radius=self.torsion_joint.radius,
))
)
if parent:
angle = - span - angle
else:
angle = self._max_contraction_angle - angle
result = result.located(Cq.Location((0,0,-self.height/2), (0,0,1), angle))
if mirror:
result = result.mirror('XZ')
return result
def parent(self, top: bool = False) -> Cq.Assembly:
joint = self.torsion_joint
# Thickness of the lip connecting this joint to the wing root
assert self.parent_lip_width <= joint.radius_track * 2
assert self.parent_arm_width <= joint.radius_track * 2
assert self.parent_lip_ext > joint.radius_track
lip_guard = (
arm = (
Cq.Solid.makeBox(
self.parent_lip_ext,
self.parent_lip_width,
self.parent_lip_guard_height)
.located(Cq.Location((0, -self.parent_lip_width/2 , 0)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height))
self.parent_lip_ext + self.parent_lip_width / 2,
self.parent_arm_width,
self.parent_arm_height)
.located(Cq.Location((0, -self.parent_arm_width/2 , 0)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_arm_height))
.cut(self._contraction_cut_geometry(parent=True, mirror=top))
)
lip = MountingBox(
lip_args = dict(
length=self.parent_lip_length,
width=self.parent_lip_width,
thickness=self.parent_lip_thickness,
hole_diam=self.parent_conn_hole_diam,
generate_side_tags=False,
)
lip1 = MountingBox(
**lip_args,
holes=[
Hole(x=self.height / 2 - x, y=-y)
for x, y in self.parent_conn_hole_pos
],
)
lip2 = MountingBox(
**lip_args,
holes=[
Hole(x=self.height / 2 - x, y=y)
for x, y in self.parent_conn_hole_pos
],
hole_diam=self.parent_conn_hole_diam,
generate_side_tags=False,
)
lip_dy = self.parent_arm_width / 2 - self.parent_lip_thickness
# Flip so the lip's holes point to -X
loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90)
# so they point to +X
loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180)
loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, 0))
loc_dir1 = Cq.Location((0,lip_dy,0), (0, 0, 1), -90)
loc_dir2 = Cq.Location((0,-lip_dy,0), (0, 0, 1), 90)
loc_pos = Cq.Location((self.parent_lip_ext, 0, 0))
rot = -self.axis_rotate_top if top else self.axis_rotate_bot
lip_p_tag, lip_n_tag = "lip_right", "lip_left"
if not top:
lip_p_tag, lip_n_tag = lip_n_tag, lip_p_tag
result = (
Cq.Assembly()
.add(joint.track(), name="track",
loc=Cq.Location((0, 0, 0), (0, 0, 1), rot))
.add(lip_guard, name="lip_guard")
.add(lip.generate(), name="lip", loc=loc_pos * loc_dir * loc_axis)
.add(arm, name="arm")
.add(lip1.generate(), name=lip_p_tag, loc=loc_pos * loc_dir1 * loc_axis)
.add(lip2.generate(), name=lip_n_tag, loc=loc_pos * loc_dir2 * loc_axis)
)
return result
@ -421,6 +480,7 @@ class ShoulderJoint(Model):
combine='cut',
centered=(False, True, True),
)
.cut(self._contraction_cut_geometry(parent=False))
)
core = (
Cq.Workplane('XY')
@ -462,7 +522,11 @@ class ShoulderJoint(Model):
return result
@assembly()
def assembly(self, fastener_pos: float = 0.0, deflection: float = 0) -> Cq.Assembly:
def assembly(
self,
fastener_pos: float = 0.0,
deflection: float = 0.0,
) -> Cq.Assembly:
assert deflection <= self.angle_max_deflection
directrix = self.directrix_id
mat = Material.RESIN_TRANSPERENT

View File

@ -116,7 +116,7 @@ class WingProfile(Model):
assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness)
self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias
self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, self.shoulder_angle_bias)
self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, 0)
self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2
assert self.spacer_thickness == self.root_joint.child_mount_thickness
@ -201,7 +201,7 @@ class WingProfile(Model):
"""
result = math.degrees(math.atan2(-self.shoulder_tip_bezier_y, self.shoulder_tip_bezier_x))
assert result >= 0
return result
return result / 2
@target(name="profile-s0", kind=TargetKind.DXF)
def profile_s0(self, top: bool = True) -> Cq.Sketch:
@ -266,17 +266,18 @@ class WingProfile(Model):
return result
@submodel(name="spacer-s0-shoulder")
def spacer_s0_shoulder(self) -> MountingBox:
def spacer_s0_shoulder(self, left: bool=True) -> MountingBox:
"""
Shoulder side serves double purpose for mounting shoulder joint and
structural support
"""
sign = 1 if left else -1
holes = [
hole
for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos)
for hole in [
Hole(x=x, y=y, tag=f"conn_top{i}"),
Hole(x=-x, y=y, tag=f"conn_bot{i}"),
Hole(x=x, y=sign * y, tag=f"conn_top{i}"),
Hole(x=-x, y=sign * y, tag=f"conn_bot{i}"),
]
]
return MountingBox(
@ -287,6 +288,7 @@ class WingProfile(Model):
hole_diam=self.shoulder_joint.parent_conn_hole_diam,
centred=(True, True),
flip_y=self.flip,
centre_bot_top_tags=True,
)
@submodel(name="spacer-s0-shoulder")
def spacer_s0_base(self) -> MountingBox:
@ -329,19 +331,19 @@ class WingProfile(Model):
def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10
base_dy = self.root_joint.hirth_joint.joint_height
loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2)
#mid_spacer_loc = (
# Cq.Location.from2d(0, -self.shoulder_width/2) *
# self.shoulder_axle_loc *
# Cq.Location.rot2d(self.shoulder_joint.angle_neutral)
#)
axle_rotate = Cq.Location.rot2d(-self.shoulder_angle_neutral)
tags = [
("shoulder",
self.shoulder_axle_loc *
self.shoulder_joint.parent_arm_loc() *
loc_tip),
("shoulder_left",
self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)),
("shoulder_right",
self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)),
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
("electronic_mount", Cq.Location.from2d(-55, 75, 64)),
("electronic_mount", Cq.Location.from2d(-45, 75, 64)),
]
result = extrude_with_markers(
self.profile_s0(top=top),
@ -377,7 +379,8 @@ class WingProfile(Model):
#.constrain("top?corner_left", "inner_shell?top", "Point")
)
for o, tag in [
(self.spacer_s0_shoulder().generate(), "shoulder"),
(self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"),
(self.spacer_s0_shoulder(left=False).generate(), "shoulder_right"),
(self.spacer_s0_base().generate(), "base"),
(self.spacer_s0_electronic_mount().generate(), "electronic_mount"),
]:
@ -902,13 +905,14 @@ class WingProfile(Model):
fastener_pos=fastener_pos,
deflection=angle), name="shoulder")
if "s0" in parts and "shoulder" in parts:
(
result
.constrain(f"s0/shoulder?conn_top0", f"shoulder/parent_{tag_top}/lip?conn0", "Plane")
.constrain(f"s0/shoulder?conn_top1", f"shoulder/parent_{tag_top}/lip?conn1", "Plane")
.constrain(f"s0/shoulder?conn_bot0", f"shoulder/parent_{tag_bot}/lip?conn0", "Plane")
.constrain(f"s0/shoulder?conn_bot1", f"shoulder/parent_{tag_bot}/lip?conn1", "Plane")
)
for i in range(len(self.shoulder_joint.parent_conn_hole_pos)):
(
result
.constrain(f"s0/shoulder_left?conn_top{i}", f"shoulder/parent_{tag_top}/lip_left?conn{i}", "Plane")
.constrain(f"s0/shoulder_left?conn_bot{i}", f"shoulder/parent_{tag_bot}/lip_left?conn{i}", "Plane")
.constrain(f"s0/shoulder_right?conn_top{i}", f"shoulder/parent_{tag_top}/lip_right?conn{i}", "Plane")
.constrain(f"s0/shoulder_right?conn_bot{i}", f"shoulder/parent_{tag_bot}/lip_right?conn{i}", "Plane")
)
if "s1" in parts:
result.add(self.assembly_s1(), name="s1")
if "s1" in parts and "shoulder" in parts: