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