cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
4 changed files with 129 additions and 54 deletions
Showing only changes of commit ac509a1625 - Show all commits

View File

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

View File

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

View File

@ -261,18 +261,23 @@ class ShoulderJoint(Model):
# On the parent side, drill vertical holes # On the parent side, drill vertical holes
parent_conn_hole_diam: float = 6.0 parent_conn_hole_diam: float = 4.0
# Position of the holes relative # Position of the holes relative centre line
parent_conn_hole_pos: list[Tuple[float, float]] = field(default_factory=lambda: [ parent_conn_hole_pos: list[Tuple[float, float]] = field(default_factory=lambda: [
(15, 8), (20, 8),
(15, -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_length: float = 25.0
parent_lip_width: float = 30.0 parent_lip_width: float = 30.0
parent_lip_thickness: float = 5.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 # Generates a child guard which covers up the internals. The lip length is
# relative to the +X surface of the guard. # relative to the +X surface of the guard.
@ -281,19 +286,19 @@ class ShoulderJoint(Model):
# guard length measured from axle # guard length measured from axle
child_lip_length: float = 40.0 child_lip_length: float = 40.0
child_lip_width: float = 20.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 # 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 child_core_thickness: float = 3.0
# Rotates the torsion joint to avoid collisions or for some other purpose # Rotates the torsion joint to avoid collisions or for some other purpose
axis_rotate_bot: float = 225.0 axis_rotate_bot: float = 90
axis_rotate_top: float = -225.0 axis_rotate_top: float = 0
directrix_id: int = 0 directrix_id: int = 0
angle_neutral: float = 10.0 angle_neutral: float = -15.0
angle_max_deflection: float = 80.0 angle_max_deflection: float = 90.0
def __post_init__(self): def __post_init__(self):
assert self.parent_lip_length * 2 < self.height assert self.parent_lip_length * 2 < self.height
@ -302,51 +307,105 @@ class ShoulderJoint(Model):
def radius(self): def radius(self):
return self.torsion_joint.radius 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 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: def parent(self, top: bool = False) -> Cq.Assembly:
joint = self.torsion_joint joint = self.torsion_joint
# Thickness of the lip connecting this joint to the wing root # 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 assert self.parent_lip_ext > joint.radius_track
lip_guard = ( arm = (
Cq.Solid.makeBox( Cq.Solid.makeBox(
self.parent_lip_ext, self.parent_lip_ext + self.parent_lip_width / 2,
self.parent_lip_width, self.parent_arm_width,
self.parent_lip_guard_height) self.parent_arm_height)
.located(Cq.Location((0, -self.parent_lip_width/2 , 0))) .located(Cq.Location((0, -self.parent_arm_width/2 , 0)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height)) .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, length=self.parent_lip_length,
width=self.parent_lip_width, width=self.parent_lip_width,
thickness=self.parent_lip_thickness, 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=[ holes=[
Hole(x=self.height / 2 - x, y=y) Hole(x=self.height / 2 - x, y=y)
for x, y in self.parent_conn_hole_pos 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 # Flip so the lip's holes point to -X
loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90) loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90)
# so they point to +X loc_dir1 = Cq.Location((0,lip_dy,0), (0, 0, 1), -90)
loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180) loc_dir2 = Cq.Location((0,-lip_dy,0), (0, 0, 1), 90)
loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, 0)) loc_pos = Cq.Location((self.parent_lip_ext, 0, 0))
rot = -self.axis_rotate_top if top else self.axis_rotate_bot 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 = ( result = (
Cq.Assembly() Cq.Assembly()
.add(joint.track(), name="track", .add(joint.track(), name="track",
loc=Cq.Location((0, 0, 0), (0, 0, 1), rot)) loc=Cq.Location((0, 0, 0), (0, 0, 1), rot))
.add(lip_guard, name="lip_guard") .add(arm, name="arm")
.add(lip.generate(), name="lip", loc=loc_pos * loc_dir * loc_axis) .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 return result
@ -421,6 +480,7 @@ class ShoulderJoint(Model):
combine='cut', combine='cut',
centered=(False, True, True), centered=(False, True, True),
) )
.cut(self._contraction_cut_geometry(parent=False))
) )
core = ( core = (
Cq.Workplane('XY') Cq.Workplane('XY')
@ -462,7 +522,11 @@ class ShoulderJoint(Model):
return result return result
@assembly() @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 assert deflection <= self.angle_max_deflection
directrix = self.directrix_id directrix = self.directrix_id
mat = Material.RESIN_TRANSPERENT 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) 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_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 self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2
assert self.spacer_thickness == self.root_joint.child_mount_thickness 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)) result = math.degrees(math.atan2(-self.shoulder_tip_bezier_y, self.shoulder_tip_bezier_x))
assert result >= 0 assert result >= 0
return result return result / 2
@target(name="profile-s0", kind=TargetKind.DXF) @target(name="profile-s0", kind=TargetKind.DXF)
def profile_s0(self, top: bool = True) -> Cq.Sketch: def profile_s0(self, top: bool = True) -> Cq.Sketch:
@ -266,17 +266,18 @@ class WingProfile(Model):
return result return result
@submodel(name="spacer-s0-shoulder") @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 Shoulder side serves double purpose for mounting shoulder joint and
structural support structural support
""" """
sign = 1 if left else -1
holes = [ holes = [
hole hole
for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos) for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos)
for hole in [ for hole in [
Hole(x=x, y=y, tag=f"conn_top{i}"), Hole(x=x, y=sign * 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_bot{i}"),
] ]
] ]
return MountingBox( return MountingBox(
@ -287,6 +288,7 @@ class WingProfile(Model):
hole_diam=self.shoulder_joint.parent_conn_hole_diam, hole_diam=self.shoulder_joint.parent_conn_hole_diam,
centred=(True, True), centred=(True, True),
flip_y=self.flip, flip_y=self.flip,
centre_bot_top_tags=True,
) )
@submodel(name="spacer-s0-shoulder") @submodel(name="spacer-s0-shoulder")
def spacer_s0_base(self) -> MountingBox: def spacer_s0_base(self) -> MountingBox:
@ -329,19 +331,19 @@ class WingProfile(Model):
def surface_s0(self, top: bool = False) -> Cq.Workplane: def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10
base_dy = self.root_joint.hirth_joint.joint_height base_dy = self.root_joint.hirth_joint.joint_height
loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2)
#mid_spacer_loc = ( #mid_spacer_loc = (
# Cq.Location.from2d(0, -self.shoulder_width/2) * # Cq.Location.from2d(0, -self.shoulder_width/2) *
# self.shoulder_axle_loc * # self.shoulder_axle_loc *
# Cq.Location.rot2d(self.shoulder_joint.angle_neutral) # Cq.Location.rot2d(self.shoulder_joint.angle_neutral)
#) #)
axle_rotate = Cq.Location.rot2d(-self.shoulder_angle_neutral)
tags = [ tags = [
("shoulder", ("shoulder_left",
self.shoulder_axle_loc * self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)),
self.shoulder_joint.parent_arm_loc() * ("shoulder_right",
loc_tip), self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)),
("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("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( result = extrude_with_markers(
self.profile_s0(top=top), self.profile_s0(top=top),
@ -377,7 +379,8 @@ class WingProfile(Model):
#.constrain("top?corner_left", "inner_shell?top", "Point") #.constrain("top?corner_left", "inner_shell?top", "Point")
) )
for o, tag in [ 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_base().generate(), "base"),
(self.spacer_s0_electronic_mount().generate(), "electronic_mount"), (self.spacer_s0_electronic_mount().generate(), "electronic_mount"),
]: ]:
@ -902,13 +905,14 @@ class WingProfile(Model):
fastener_pos=fastener_pos, fastener_pos=fastener_pos,
deflection=angle), name="shoulder") deflection=angle), name="shoulder")
if "s0" in parts and "shoulder" in parts: if "s0" in parts and "shoulder" in parts:
( for i in range(len(self.shoulder_joint.parent_conn_hole_pos)):
result (
.constrain(f"s0/shoulder?conn_top0", f"shoulder/parent_{tag_top}/lip?conn0", "Plane") result
.constrain(f"s0/shoulder?conn_top1", f"shoulder/parent_{tag_top}/lip?conn1", "Plane") .constrain(f"s0/shoulder_left?conn_top{i}", f"shoulder/parent_{tag_top}/lip_left?conn{i}", "Plane")
.constrain(f"s0/shoulder?conn_bot0", f"shoulder/parent_{tag_bot}/lip?conn0", "Plane") .constrain(f"s0/shoulder_left?conn_bot{i}", f"shoulder/parent_{tag_bot}/lip_left?conn{i}", "Plane")
.constrain(f"s0/shoulder?conn_bot1", f"shoulder/parent_{tag_bot}/lip?conn1", "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: if "s1" in parts:
result.add(self.assembly_s1(), name="s1") result.add(self.assembly_s1(), name="s1")
if "s1" in parts and "shoulder" in parts: if "s1" in parts and "shoulder" in parts: