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