cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
5 changed files with 155 additions and 31 deletions
Showing only changes of commit c1107aed2e - Show all commits

View File

@ -8,7 +8,7 @@ import nhf.utils
TOL = 1e-6
@dataclass
@dataclass(frozen=True)
class HirthJoint:
"""
A Hirth joint attached to a cylindrical base

View File

@ -33,7 +33,6 @@ from dataclasses import dataclass, field
from typing import Optional
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.parts.joints import HirthJoint, TorsionJoint
import nhf.touhou.houjuu_nue.wing as MW
import nhf.touhou.houjuu_nue.trident as MT
import nhf.touhou.houjuu_nue.joints as MJ
@ -53,6 +52,10 @@ class Parameters(Model):
wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r1",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(0,1,1,1),
parent_substrate_cull_edges=(0,0,1,0),
),
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=False,
s0_bot_hole=True,
@ -60,18 +63,30 @@ class Parameters(Model):
))
wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r2",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,1),
parent_substrate_cull_edges=(0,0,1,0),
),
shoulder_angle_bias=WING_DEFLECT_EVEN,
s0_top_hole=True,
s0_bot_hole=True,
))
wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r3",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,0),
parent_substrate_cull_edges=(0,0,1,0),
),
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=True,
s0_bot_hole=False,
))
wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l1",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,0,1,1),
parent_substrate_cull_edges=(1,0,0,0),
),
shoulder_angle_bias=WING_DEFLECT_EVEN,
wrist_angle=-60.0,
s0_top_hole=False,
@ -79,6 +94,10 @@ class Parameters(Model):
))
wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l2",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,1),
parent_substrate_cull_edges=(1,0,0,0),
),
wrist_angle=-30.0,
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=True,
@ -86,6 +105,10 @@ class Parameters(Model):
))
wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l3",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,0,1),
parent_substrate_cull_edges=(1,0,0,0),
),
shoulder_angle_bias=WING_DEFLECT_EVEN,
wrist_angle=-0.0,
s0_top_hole=True,
@ -96,14 +119,6 @@ class Parameters(Model):
def __post_init__(self):
super().__init__(name="houjuu-nue")
self.wing_r1.root_joint = self.harness.root_joint
self.wing_r2.root_joint = self.harness.root_joint
self.wing_r3.root_joint = self.harness.root_joint
self.wing_l1.root_joint = self.harness.root_joint
self.wing_l2.root_joint = self.harness.root_joint
self.wing_l3.root_joint = self.harness.root_joint
self.wing_r1.shoulder_joint.torsion_joint
@submodel(name="harness")
def submodel_harness(self) -> Model:

View File

@ -4,6 +4,7 @@ from nhf.parts.joints import HirthJoint
from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.touhou.houjuu_nue.joints import RootJoint
from nhf.parts.box import MountingBox
import nhf.utils
@dataclass(frozen=True, kw_only=True)
@ -46,21 +47,21 @@ class Mannequin:
return result.translate((0, self.torso_thickness / 2, 0))
BASE_POS_X = 60.0
BASE_POS_X = 70.0
BASE_POS_Y = 100.0
@dataclass(kw_only=True)
class Harness(Model):
thickness: float = 25.4 / 8
width: float = 220.0
height: float = 310.0
height: float = 304.8
fillet: float = 10.0
wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [
("r1", BASE_POS_X + 10, BASE_POS_Y),
("l1", -BASE_POS_X - 10, BASE_POS_Y),
("r2", BASE_POS_X + 10, 0),
("l2", -BASE_POS_X - 10, 0),
("r1", BASE_POS_X, BASE_POS_Y),
("l1", -BASE_POS_X, BASE_POS_Y),
("r2", BASE_POS_X, 0),
("l2", -BASE_POS_X, 0),
("r3", BASE_POS_X, -BASE_POS_Y),
("l3", -BASE_POS_X, -BASE_POS_Y),
])
@ -72,9 +73,12 @@ class Harness(Model):
def __post_init__(self):
super().__init__(name="harness")
@submodel(name="root-joint")
def submodel_root_joint(self) -> Model:
return self.root_joint
@submodel(name="bridge-pair-horizontal")
def bridge_pair_horizontal(self) -> MountingBox:
return self.root_joint.bridge_pair_horizontal(centre_dx=BASE_POS_X * 2)
@submodel(name="bridge-pair-vertical")
def bridge_pair_vertical(self) -> MountingBox:
return self.root_joint.bridge_pair_vertical(centre_dy=BASE_POS_Y)
@target(name="profile", kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch:
@ -126,7 +130,7 @@ class Harness(Model):
conn = [(px + x, py + y) for px, py
in self.root_joint.corner_pos()]
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPoint(f"{tag}_{i}")
plane.moveTo(px, py).tagPlane(f"{tag}_{i}")
return result
def add_root_joint_constraint(
@ -144,7 +148,6 @@ class Harness(Model):
harness = self.surface()
mannequin_z = self.mannequin.shoulder_to_waist * 0.6
result = (
Cq.Assembly()
.addS(
@ -159,6 +162,37 @@ class Harness(Model):
loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180))
.constrain("mannequin", "Fixed")
)
bridge_h = self.bridge_pair_horizontal().generate()
for i in [1,2,3]:
name = f"r{i}l{i}_bridge"
(
result
.addS(
bridge_h, name=name,
role=Role.FIXTURE,
material=Material.WOOD_BIRCH,
)
.constrain(f"{name}?conn0_rev", f"base?r{i}_1", "Point")
.constrain(f"{name}?conn1_rev", f"base?l{i}_0", "Point")
.constrain(f"{name}?conn2_rev", f"base?l{i}_3", "Point")
.constrain(f"{name}?conn3_rev", f"base?r{i}_2", "Point")
)
bridge_v = self.bridge_pair_vertical().generate()
(
result
.addS(bridge_v, name="r1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("r1_bridge?conn0_rev", "base?r1_3", 'Plane')
.constrain("r1_bridge?conn1_rev", "base?r2_0", 'Plane')
.addS(bridge_v, name="r2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("r2_bridge?conn0_rev", "base?r2_3", 'Plane')
.constrain("r2_bridge?conn1_rev", "base?r3_0", 'Plane')
.addS(bridge_v, name="l1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("l1_bridge?conn0_rev", "base?l1_2", 'Plane')
.constrain("l1_bridge?conn1_rev", "base?l2_1", 'Plane')
.addS(bridge_v, name="l2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("l2_bridge?conn0_rev", "base?l2_2", 'Plane')
.constrain("l2_bridge?conn1_rev", "base?l3_1", 'Plane')
)
if with_root_joint:
for name in ["l1", "l2", "l3", "r1", "r2", "r3"]:
result.addS(

View File

@ -4,6 +4,7 @@ from typing import Optional, Tuple
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
from nhf.parts.box import MountingBox
from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.joints import TorsionJoint, HirthJoint
@ -88,6 +89,10 @@ class RootJoint(Model):
parent_corner_inset: float = 12
parent_mount_thickness: float = 25.4 / 16
parent_substrate_thickness: float = 25.4 / 16
parent_substrate_cull_corners: Tuple[bool, bool, bool, bool] = (False, False, True, False)
parent_substrate_cull_edges: Tuple[bool, bool, bool, bool] = (False, False, True, False)
child_corner_dx: float = 17.0
child_corner_dz: float = 24.0
@ -103,6 +108,8 @@ class RootJoint(Model):
def __post_init__(self):
assert self.child_extra_thickness > 0.0
assert self.parent_thickness >= self.hex_nut.thickness
# twice the buffer allocated for substratum
assert self.parent_width >= 4 * self.parent_corner_inset
def corner_pos(self) -> list[tuple[int, int]]:
"""
@ -111,9 +118,9 @@ class RootJoint(Model):
dx = self.parent_width / 2 - self.parent_corner_inset
return [
(dx, dx),
(dx, -dx),
(-dx, -dx),
(-dx, dx),
(-dx, -dx),
(dx, -dx),
]
@property
@ -127,6 +134,45 @@ class RootJoint(Model):
def base_to_surface_thickness(self) -> float:
return self.hirth_joint.joint_height + self.child_extra_thickness
@property
def substrate_inset(self) -> float:
return self.parent_corner_inset * 2
def bridge_pair_horizontal(self, centre_dx: float) -> MountingBox:
hole_dx = centre_dx / 2 - self.parent_width / 2 + self.parent_corner_inset
hole_dy = self.parent_width / 2 - self.parent_corner_inset
holes = [
Hole(x=hole_dx, y=hole_dy),
Hole(x=-hole_dx, y=hole_dy),
Hole(x=-hole_dx, y=-hole_dy),
Hole(x=hole_dx, y=-hole_dy),
]
return MountingBox(
length=centre_dx - self.parent_width + self.substrate_inset * 2,
width=self.parent_width,
thickness=self.parent_substrate_thickness,
hole_diam=self.corner_hole_diam,
holes=holes,
centred=(True, True),
generate_reverse_tags=True,
)
def bridge_pair_vertical(self, centre_dy: float) -> MountingBox:
hole_dy = centre_dy / 2 - self.parent_width / 2 + self.parent_corner_inset
holes = [
Hole(x=0, y=hole_dy),
Hole(x=0, y=-hole_dy),
]
return MountingBox(
length=self.substrate_inset,
width=centre_dy - self.parent_width + self.substrate_inset * 2,
thickness=self.parent_substrate_thickness,
hole_diam=self.corner_hole_diam,
holes=holes,
centred=(True, True),
generate_reverse_tags=True,
)
@target(name="parent")
def parent(self):
"""
@ -142,7 +188,6 @@ class RootJoint(Model):
width=self.parent_width,
height=self.parent_thickness,
centered=(True, True, False))
.translate((0, 0, -self.parent_thickness))
.edges("|Z")
.fillet(self.parent_corner_fillet)
.faces(">Z")
@ -153,6 +198,32 @@ class RootJoint(Model):
cboreDiameter=self.parent_corner_cbore_diam,
cboreDepth=self.parent_corner_cbore_depth)
)
sso = self.parent_width / 2 - self.substrate_inset
loc_corner = Cq.Location((sso, sso, 0))
loc_edge= Cq.Location((sso, -sso, 0))
cut_corner = Cq.Solid.makeBox(
length=self.parent_width,
width=self.parent_width,
height=self.parent_substrate_thickness,
)
cut_edge = Cq.Solid.makeBox(
length=self.parent_width,
width=self.parent_width - self.substrate_inset * 2,
height=self.parent_substrate_thickness,
)
step = 90
for i, flag in enumerate(self.parent_substrate_cull_corners):
if not flag:
continue
loc = Cq.Location((0,0,0),(0,0,1), i * step) * loc_corner
result = result.cut(cut_corner.located(loc))
for i, flag in enumerate(self.parent_substrate_cull_edges):
if not flag:
continue
loc = Cq.Location((0,0,0),(0,0,1), i * step) * loc_edge
result = result.cut(cut_edge.located(loc))
result = result.translate((0, 0, -self.parent_thickness))
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.parent_thickness)
@ -171,7 +242,7 @@ class RootJoint(Model):
.hole(diameter=self.axis_diam)
.cut(self.hex_nut.generate().translate((0, 0, -self.parent_thickness)))
)
result.faces("<Z").tag("base")
result.copyWorkplane(Cq.Workplane('XY', origin=(0,0,-self.parent_thickness))).tagPlane("base", direction="-Z")
return result
@target(name="child")

View File

@ -142,6 +142,9 @@ class WingProfile(Model):
extra = 2 * self.panel_thickness if self.flip else 0
return self.s1_thickness - 2 * self.panel_thickness - extra
@submodel(name="root-joint")
def submodel_root_joint(self) -> Model:
return self.root_joint
@submodel(name="shoulder-joint")
def submodel_shoulder_joint(self) -> Model:
return self.shoulder_joint
@ -312,6 +315,7 @@ class WingProfile(Model):
Hole(x=dx, y=-dy),
Hole(x=dx, y=dy),
Hole(x=-dx, y=dy),
Hole(x=0, y=0, diam=self.root_joint.axis_diam, tag="axle"),
]
return MountingBox(
length=self.root_height,