cosplay: Touhou/Houjuu Nue #4
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue