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

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

View File

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