cosplay: Touhou/Houjuu Nue #4
|
@ -34,6 +34,11 @@ class Hole:
|
|||
diam: Optional[float] = None
|
||||
tag: Optional[str] = None
|
||||
|
||||
@property
|
||||
def rev_tag(self) -> str:
|
||||
assert self.tag is not None
|
||||
return self.tag + "_rev"
|
||||
|
||||
@dataclass
|
||||
class MountingBox(Model):
|
||||
"""
|
||||
|
@ -56,6 +61,11 @@ class MountingBox(Model):
|
|||
# Determines the position of side tags
|
||||
flip_y: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
for i, hole in enumerate(self.holes):
|
||||
if hole.tag is None:
|
||||
hole.tag = f"conn{i}"
|
||||
|
||||
@target(kind=TargetKind.DXF)
|
||||
def profile(self) -> Cq.Sketch:
|
||||
bx, by = 0, 0
|
||||
|
@ -84,12 +94,11 @@ class MountingBox(Model):
|
|||
)
|
||||
plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness)
|
||||
reverse_plane = result.copyWorkplane(Cq.Workplane('XY'))
|
||||
for i, hole in enumerate(self.holes):
|
||||
tag = hole.tag if hole.tag else f"conn{i}"
|
||||
plane.moveTo(hole.x, hole.y).tagPlane(tag)
|
||||
for hole in self.holes:
|
||||
assert hole.tag
|
||||
plane.moveTo(hole.x, hole.y).tagPlane(hole.tag)
|
||||
if self.generate_reverse_tags:
|
||||
rev_tag = hole.tag + "_rev" if hole.tag else f"conn{i}_rev"
|
||||
reverse_plane.moveTo(hole.x, hole.y).tagPlane(rev_tag, '-Z')
|
||||
reverse_plane.moveTo(hole.x, hole.y).tagPlane(hole.rev_tag, '-Z')
|
||||
|
||||
if self.generate_side_tags:
|
||||
result.faces("<Y").workplane(origin=result.vertices("<X and <Y and >Z").val().Center()).tagPlane("left")
|
||||
|
@ -106,10 +115,10 @@ class MountingBox(Model):
|
|||
Cq.Assembly()
|
||||
.add(self.generate(), name="box")
|
||||
)
|
||||
for i in range(len(self.holes)):
|
||||
result.markPlane(f"box?conn{i}")
|
||||
for hole in self.holes:
|
||||
result.markPlane(f"box?{hole.tag}")
|
||||
if self.generate_reverse_tags:
|
||||
result.markPlane(f"box?conn{i}_rev")
|
||||
result.markPlane(f"box?{hole.rev_tag}")
|
||||
if self.generate_side_tags:
|
||||
(
|
||||
result
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
|
||||
|
||||
NUT_COMMON = HexNut(
|
||||
# FIXME: measure
|
||||
mass=0.0,
|
||||
diam_thread=4.0,
|
||||
pitch=0.7,
|
||||
thickness=3.2,
|
||||
width=7.0,
|
||||
)
|
||||
BOLT_COMMON = FlatHeadBolt(
|
||||
# FIXME: measure
|
||||
mass=0.0,
|
||||
diam_head=8.0,
|
||||
height_head=2.0,
|
||||
diam_thread=4.0,
|
||||
height_thread=20.0,
|
||||
)
|
|
@ -1,13 +1,16 @@
|
|||
"""
|
||||
Electronic components
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Tuple
|
||||
import math
|
||||
import cadquery as Cq
|
||||
from nhf.materials import Role
|
||||
from nhf.build import Model, TargetKind, target, assembly, submodel
|
||||
from nhf.materials import Role, Material
|
||||
from nhf.parts.box import MountingBox, Hole
|
||||
from nhf.parts.item import Item
|
||||
from nhf.parts.fasteners import FlatHeadBolt, HexNut
|
||||
from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON
|
||||
import nhf.utils
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
@ -268,8 +271,6 @@ class BatteryBox18650(Item):
|
|||
)
|
||||
|
||||
|
||||
|
||||
|
||||
LINEAR_ACTUATOR_50 = LinearActuator(
|
||||
mass=34.0,
|
||||
stroke_length=50,
|
||||
|
@ -427,6 +428,65 @@ class Flexor:
|
|||
|
||||
|
||||
@dataclass
|
||||
class ElectronicBoard:
|
||||
class ElectronicBoard(Model):
|
||||
|
||||
hole_diam: float = 4.0
|
||||
name: str = "electronic-board"
|
||||
nut: HexNut = NUT_COMMON
|
||||
bolt: FlatHeadBolt = BOLT_COMMON
|
||||
length: float = 70.0
|
||||
width: float = 170.0
|
||||
mount_holes: list[Hole] = field(default_factory=lambda: [
|
||||
Hole(x=30, y=80),
|
||||
Hole(x=30, y=-80),
|
||||
Hole(x=-30, y=80),
|
||||
Hole(x=-30, y=-80),
|
||||
])
|
||||
panel_thickness: float = 25.4 / 16
|
||||
mount_panel_thickness: float = 25.4 / 4
|
||||
material: Material = Material.WOOD_BIRCH
|
||||
|
||||
@property
|
||||
def mount_hole_diam(self) -> float:
|
||||
return self.bolt.diam_thread
|
||||
|
||||
def __post_init__(self):
|
||||
super().__init__(name=self.name)
|
||||
|
||||
@submodel(name="panel")
|
||||
def panel(self) -> MountingBox:
|
||||
return MountingBox(
|
||||
holes=self.mount_holes,
|
||||
hole_diam=self.mount_hole_diam,
|
||||
length=self.length,
|
||||
width=self.width,
|
||||
centred=(True, True),
|
||||
thickness=self.panel_thickness,
|
||||
generate_reverse_tags=True,
|
||||
)
|
||||
|
||||
def assembly(self) -> Cq.Assembly:
|
||||
panel = self.panel()
|
||||
result = (
|
||||
Cq.Assembly()
|
||||
.addS(panel.generate(), name="panel",
|
||||
role=Role.STRUCTURE, material=self.material)
|
||||
)
|
||||
for hole in panel.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}",
|
||||
"Plane", param=0
|
||||
)
|
||||
)
|
||||
return result.solve()
|
||||
|
|
|
@ -90,7 +90,7 @@ class WingProfile(Model):
|
|||
elbow_rotate: float = -5.0
|
||||
wrist_rotate: float = -30.0
|
||||
# Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side)
|
||||
elbow_axle_pos: float = 0.3
|
||||
elbow_axle_pos: float = 0.5
|
||||
wrist_axle_pos: float = 0.0
|
||||
|
||||
# False for the right side, True for the left side
|
||||
|
@ -99,6 +99,8 @@ class WingProfile(Model):
|
|||
def __post_init__(self):
|
||||
super().__init__(name=self.name)
|
||||
|
||||
assert self.electronic_board.length == self.shoulder_height
|
||||
|
||||
self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height)
|
||||
self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height)
|
||||
if self.flip:
|
||||
|
@ -114,6 +116,7 @@ class WingProfile(Model):
|
|||
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_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2
|
||||
|
||||
assert self.spacer_thickness == self.root_joint.child_mount_thickness
|
||||
|
||||
@property
|
||||
|
@ -278,21 +281,16 @@ class WingProfile(Model):
|
|||
flip_y=self.flip,
|
||||
)
|
||||
@submodel(name="spacer-s0-electronic")
|
||||
def spacer_s0_electronic(self) -> MountingBox:
|
||||
holes = [
|
||||
Hole(x=30, y=80),
|
||||
Hole(x=30, y=-80),
|
||||
Hole(x=-30, y=80),
|
||||
Hole(x=-30, y=-80),
|
||||
]
|
||||
def spacer_s0_electronic_mount(self) -> MountingBox:
|
||||
return MountingBox(
|
||||
holes=holes,
|
||||
hole_diam=self.electronic_board.hole_diam,
|
||||
holes=self.electronic_board.mount_holes,
|
||||
hole_diam=self.electronic_board.mount_hole_diam,
|
||||
length=self.root_height,
|
||||
width=170,
|
||||
width=self.electronic_board.width,
|
||||
centred=(True, True),
|
||||
thickness=self.spacer_thickness,
|
||||
flip_y=self.flip
|
||||
flip_y=self.flip,
|
||||
generate_reverse_tags=True,
|
||||
)
|
||||
|
||||
def surface_s0(self, top: bool = False) -> Cq.Workplane:
|
||||
|
@ -310,7 +308,7 @@ class WingProfile(Model):
|
|||
self.shoulder_joint.parent_arm_loc() *
|
||||
loc_tip),
|
||||
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
|
||||
("electronic", Cq.Location.from2d(-55, 75, 64)),
|
||||
("electronic_mount", Cq.Location.from2d(-55, 75, 64)),
|
||||
]
|
||||
result = extrude_with_markers(
|
||||
self.profile_s0(top=top),
|
||||
|
@ -323,7 +321,7 @@ class WingProfile(Model):
|
|||
return result
|
||||
|
||||
@assembly()
|
||||
def assembly_s0(self) -> Cq.Assembly:
|
||||
def assembly_s0(self, ignore_detail: bool=False) -> Cq.Assembly:
|
||||
result = (
|
||||
Cq.Assembly()
|
||||
.addS(self.surface_s0(top=True), name="bot",
|
||||
|
@ -343,7 +341,7 @@ class WingProfile(Model):
|
|||
for o, tag in [
|
||||
(self.spacer_s0_shoulder().generate(), "shoulder"),
|
||||
(self.spacer_s0_base().generate(), "base"),
|
||||
(self.spacer_s0_electronic().generate(), "electronic"),
|
||||
(self.spacer_s0_electronic_mount().generate(), "electronic_mount"),
|
||||
]:
|
||||
top_tag, bot_tag = "top", "bot"
|
||||
if self.flip:
|
||||
|
@ -357,6 +355,27 @@ class WingProfile(Model):
|
|||
.constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane")
|
||||
.constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis")
|
||||
)
|
||||
if not ignore_detail:
|
||||
result.add(self.electronic_board.assembly(), name="electronic_board")
|
||||
for hole in self.electronic_board.mount_holes:
|
||||
assert hole.tag
|
||||
nut_name = f"electronic_board_{hole.tag}_nut"
|
||||
(
|
||||
result
|
||||
.addS(
|
||||
self.electronic_board.nut.assembly(),
|
||||
name=nut_name)
|
||||
.constrain(
|
||||
f"electronic_mount?{hole.rev_tag}",
|
||||
f'{nut_name}?top',
|
||||
"Plane"
|
||||
)
|
||||
.constrain(
|
||||
f"electronic_mount?{hole.tag}",
|
||||
f'electronic_board/{hole.tag}_spacer?bot',
|
||||
"Plane"
|
||||
)
|
||||
)
|
||||
return result.solve()
|
||||
|
||||
|
||||
|
@ -804,6 +823,7 @@ class WingProfile(Model):
|
|||
elbow_wrist_deflection: float = 0.0,
|
||||
root_offset: int = 5,
|
||||
fastener_pos: float = 0.0,
|
||||
ignore_detail: bool = False,
|
||||
) -> Cq.Assembly():
|
||||
if parts is None:
|
||||
parts = [
|
||||
|
@ -824,7 +844,7 @@ class WingProfile(Model):
|
|||
tag_top, tag_bot = tag_bot, tag_top
|
||||
|
||||
if "s0" in parts:
|
||||
result.add(self.assembly_s0(), name="s0")
|
||||
result.add(self.assembly_s0(ignore_detail=ignore_detail), name="s0")
|
||||
if "root" in parts:
|
||||
result.addS(self.root_joint.assembly(
|
||||
offset=root_offset,
|
||||
|
|
Loading…
Reference in New Issue