cosplay: Touhou/Houjuu Nue #4

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

View File

@ -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

View File

@ -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,
)

View File

@ -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()

View File

@ -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,