cosplay: Touhou/Houjuu Nue #4

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

View File

@ -21,7 +21,7 @@ class ArduinoUnoR3(Item):
# This is labeled in mirrored coordinates from top down (i.e. unmirrored from bottom up)
holes: list[Tuple[float, float]] = field(default_factory=lambda: [
(15.24, 2.54),
(13.74, 50.80), # x coordinate not labeled on schematic
(15.24 - 1.270, 50.80), # x coordinate not labeled on schematic
(66.04, 17.78),
(66.04, 45.72),
])
@ -70,3 +70,66 @@ class ArduinoUnoR3(Item):
for i, (x, y) in enumerate(self.holes):
plane.moveTo(x, self.width - y).tagPlane(f"conn{i}", direction='-Z')
return result
@dataclass(frozen=True)
class BatteryBox18650(Item):
"""
A number of 18650 batteries in series
"""
mass: float = 17.4 + 68.80 * 3
length: float = 75.70
width_base: float = 61.46 - 18.48 - 20.18 * 2
battery_dist: float = 20.18
height: float = 19.66
# space from bottom to battery begin
thickness: float = 1.66
battery_diam: float = 18.48
battery_height: float = 68.80
n_batteries: int = 3
def __post_init__(self):
assert 2 * self.thickness < min(self.length, self.height)
@property
def name(self) -> str:
return f"BatteryBox 18650*{self.n_batteries}"
@property
def role(self) -> Role:
return Role.ELECTRONIC
def generate(self) -> Cq.Workplane:
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
return (
Cq.Workplane('XY')
.box(
length=self.length,
width=width,
height=self.height,
centered=(True, True, False),
)
.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.thickness)))
.box(
length=self.length - self.thickness*2,
width=width - self.thickness*2,
height=self.height - self.thickness,
centered=(True, True, False),
combine='cut',
)
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2)))
.rarray(
xSpacing=1,
ySpacing=self.battery_dist,
xCount=1,
yCount=self.n_batteries,
center=True,
)
.cylinder(
radius=self.battery_diam/2,
height=self.battery_height,
direct=(1, 0, 0),
centered=(True, True, False),
combine=True,
)
)

View File

@ -11,7 +11,7 @@ from nhf.parts.box import MountingBox, Hole
from nhf.parts.fibre import tension_fibre
from nhf.parts.item import Item
from nhf.parts.fasteners import FlatHeadBolt, HexNut
from nhf.parts.electronics import ArduinoUnoR3
from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650
from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON
import nhf.utils
@ -210,68 +210,6 @@ class MountingBracket(Item):
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid")
return result
@dataclass(frozen=True)
class BatteryBox18650(Item):
"""
A number of 18650 batteries in series
"""
mass: float = 17.4 + 68.80 * 3
length: float = 75.70
width_base: float = 61.46 - 18.48 - 20.18 * 2
battery_dist: float = 20.18
height: float = 19.66
# space from bottom to battery begin
thickness: float = 1.66
battery_diam: float = 18.48
battery_height: float = 68.80
n_batteries: int = 3
def __post_init__(self):
assert 2 * self.thickness < min(self.length, self.height)
@property
def name(self) -> str:
return f"BatteryBox 18650*{self.n_batteries}"
@property
def role(self) -> Role:
return Role.ELECTRONIC
def generate(self) -> Cq.Workplane:
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
return (
Cq.Workplane('XY')
.box(
length=self.length,
width=width,
height=self.height,
centered=(True, True, False),
)
.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.thickness)))
.box(
length=self.length - self.thickness*2,
width=width - self.thickness*2,
height=self.height - self.thickness,
centered=(True, True, False),
combine='cut',
)
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2)))
.rarray(
xSpacing=1,
ySpacing=self.battery_dist,
xCount=1,
yCount=self.n_batteries,
center=True,
)
.cylinder(
radius=self.battery_diam/2,
height=self.battery_height,
direct=(1, 0, 0),
centered=(True, True, False),
combine=True,
)
)
LINEAR_ACTUATOR_50 = LinearActuator(
mass=40.8,
@ -345,6 +283,15 @@ ELECTRONIC_MOUNT_HEXNUT = HexNut(
width=6.81,
)
@dataclass(kw_only=True, frozen=True)
class Winch:
linear_motion_span: float
actuator: LinearActuator = LINEAR_ACTUATOR_21
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
@dataclass(kw_only=True)
class Flexor:
"""
@ -529,17 +476,10 @@ class ElectronicBoard(Model):
role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material)
)
for hole in self.mount_holes:
spacer_name = f"{hole.tag}_spacer"
bolt_name = f"{hole.tag}_bolt"
(
result
.add(self.nut.assembly(), name=spacer_name)
.add(self.bolt.assembly(), name=bolt_name)
.constrain(
f"{spacer_name}?top",
f"panel?{hole.rev_tag}",
"Plane"
)
.constrain(
f"{bolt_name}?root",
f"panel?{hole.tag}",
@ -551,6 +491,7 @@ class ElectronicBoard(Model):
@dataclass
class ElectronicBoardBattery(ElectronicBoard):
name: str = "electronic-board-battery"
battery_box: BatteryBox18650 = BATTERY_BOX
@submodel(name="panel")
def panel_out(self) -> MountingBox:

View File

@ -10,7 +10,7 @@ from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob, Washer
from nhf.parts.joints import TorsionJoint, HirthJoint
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
from nhf.touhou.houjuu_nue.electronics import (
Flexor, LinearActuator, LINEAR_ACTUATOR_21,
Winch, Flexor, LinearActuator, LINEAR_ACTUATOR_21,
)
import nhf.geometry
import nhf.utils
@ -390,7 +390,7 @@ class ShoulderJoint(Model):
parent_arm_width: float = 25.0
parent_arm_height: float = 12.0
# remove a bit of material from the base so it does not interfere with gluing
parent_arm_base_shift: float = 2.0
parent_arm_base_shift: float = 1.0
# Generates a child guard which covers up the internals. The lip length is
# relative to the +X surface of the guard.
@ -423,6 +423,7 @@ class ShoulderJoint(Model):
spool_groove_radius: float = 10.0
flip: bool = False
winch: Optional[Winch] = None # Initialized later
actuator: LinearActuator = LINEAR_ACTUATOR_21
def __post_init__(self):
@ -431,6 +432,10 @@ class ShoulderJoint(Model):
assert self.spool_groove_radius < self.spool_inner_radius < self.spool_outer_radius
assert self.child_lip_height < self.height
assert self.draft_length <= self.actuator.stroke_length
self.winch = Winch(
actuator=self.actuator,
linear_motion_span=self.draft_length,
)
@property
def spool_outer_radius(self):
@ -457,6 +462,10 @@ class ShoulderJoint(Model):
"""
return 0
@property
def parent_lip_gap(self):
return self.height - self.parent_lip_length * 2
def parent_lip_loc(self, left: bool=True) -> Cq.Location:
"""
2d location of the arm surface on the parent side, relative to axle

View File

@ -21,6 +21,7 @@ from nhf.touhou.houjuu_nue.electronics import (
ElectronicBoard,
ElectronicBoardBattery,
LightStrip,
ElectronicBoard,
ELECTRONIC_MOUNT_HEXNUT,
)
import nhf.utils
@ -287,7 +288,7 @@ class WingProfile(Model):
plane.moveTo(t, self.root_height + t*2).tagPlane("top")
return result
@submodel(name="spacer-s0-shoulder")
@submodel(name="spacer-s0-shoulder-inner")
def spacer_s0_shoulder(self, left: bool=True) -> MountingBox:
"""
Shoulder side serves double purpose for mounting shoulder joint and
@ -302,6 +303,21 @@ class WingProfile(Model):
Hole(x=-x, y=sign * y, tag=f"conn_bot{i}"),
]
]
def post(sketch: Cq.Sketch) -> Cq.Sketch:
"""
Carve out the middle if this is closer to the front
"""
if left:
return sketch
return (
sketch
.push([(0,0)])
.rect(
w=self.shoulder_joint.parent_lip_gap,
h=self.shoulder_joint.parent_lip_width,
mode='s'
)
)
return MountingBox(
length=self.shoulder_joint.height,
width=self.shoulder_joint.parent_lip_width,
@ -311,7 +327,12 @@ class WingProfile(Model):
centred=(True, True),
flip_y=self.flip,
centre_bot_top_tags=True,
profile_callback=post,
)
@submodel(name="spacer-s0-shoulder-outer")
def spacer_s0_shoulder_outer(self) -> MountingBox:
return self.spacer_s0_shoulder_inner(left=False)
@submodel(name="spacer-s0-base")
def spacer_s0_base(self) -> MountingBox:
"""
@ -341,11 +362,15 @@ class WingProfile(Model):
@submodel(name="spacer-s0-electronic")
def spacer_s0_electronic_mount(self) -> MountingBox:
"""
This one has circular holes for the screws
This one has hexagonal holes
"""
face = ELECTRONIC_MOUNT_HEXNUT.cutting_face()
holes = [
Hole(x=h.x, y=h.y, face=face, tag=h.tag)
for h in self.electronic_board.mount_holes
]
return MountingBox(
holes=self.electronic_board.mount_holes,
hole_diam=self.electronic_board.mount_hole_diam,
holes=holes,
length=self.root_height,
width=self.electronic_board.width,
centred=(True, True),
@ -356,13 +381,8 @@ class WingProfile(Model):
@submodel(name="spacer-s0-electronic2")
def spacer_s0_electronic_mount2(self) -> MountingBox:
"""
This one has hexagonal holes
This one has circular holes
"""
face = ELECTRONIC_MOUNT_HEXNUT.cutting_face()
holes = [
Hole(x=h.x, y=h.y, face=face, tag=h.tag)
for h in self.electronic_board.mount_holes
]
def post(sketch: Cq.Sketch) -> Cq.Sketch:
return (
sketch
@ -370,7 +390,7 @@ class WingProfile(Model):
.rect(70, 130, mode='s')
)
return MountingBox(
holes=holes,
holes=self.electronic_board.mount_holes,
hole_diam=self.electronic_board.mount_hole_diam,
length=self.root_height,
width=self.electronic_board.width,
@ -408,7 +428,7 @@ class WingProfile(Model):
("shoulder_right",
self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)),
("shoulder_act",
self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(100, -20)),
self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(120, -40)),
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
("electronic_mount", Cq.Location.from2d(-35, 65, 60)),
]
@ -474,17 +494,26 @@ class WingProfile(Model):
for hole in self.electronic_board.mount_holes:
result.constrain(
f"electronic_mount?{hole.tag}",
f"electronic_mount2?{hole.tag}_rev",
f"electronic_mount2?{hole.rev_tag}",
"Plane")
if not ignore_electronics:
result.add(self.electronic_board.assembly(), name="electronic_board")
for hole in self.electronic_board.mount_holes:
assert hole.tag
nut_name = f"{hole.tag}_nut"
(
result
.add(self.electronic_board.nut.assembly(), name=nut_name)
.constrain(
f"{nut_name}?top",
f"electronic_mount?{hole.tag}",
"Plane", param=0
)
)
result.constrain(
f"electronic_mount2?{hole.tag}",
f'electronic_board/{hole.tag}_spacer?top',
f'electronic_board/panel?{hole.rev_tag}',
"Plane",
param=0
)
return result.solve()
@ -1073,6 +1102,27 @@ class WingProfile(Model):
result.add(self.assembly_s0(
ignore_electronics=ignore_electronics
), name="s0")
if not ignore_electronics:
tag_act = "shoulder_act"
tag_bolt = "shoulder_act_bolt"
tag_nut = "shoulder_act_nut"
tag_bracket = "shoulder_act_bracket"
winch = self.shoulder_joint.winch
(
result
.add(winch.actuator.assembly(pos=0), name=tag_act)
.add(winch.bracket.assembly(), name=tag_bracket)
.add(winch.bolt.assembly(), name=tag_bolt)
.add(winch.nut.assembly(), name=tag_nut)
.constrain(f"{tag_bolt}?root", f"{tag_bracket}?conn_top",
"Plane", param=0)
.constrain(f"{tag_nut}?bot", f"{tag_bracket}?conn_bot",
"Plane")
.constrain(f"{tag_act}/back?conn", f"{tag_bracket}?conn_mid",
"Plane", param=0)
.constrain("s0/shoulder_act?conn0", f"{tag_bracket}?conn_side",
"Plane")
)
if "root" in parts:
result.addS(self.root_joint.assembly(
offset=root_offset,