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 TOL = 1e-6
@dataclass @dataclass(frozen=True)
class HirthJoint: class HirthJoint:
""" """
A Hirth joint attached to a cylindrical base A Hirth joint attached to a cylindrical base

View File

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

View File

@ -4,6 +4,7 @@ from nhf.parts.joints import HirthJoint
from nhf import Material, Role from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.touhou.houjuu_nue.joints import RootJoint from nhf.touhou.houjuu_nue.joints import RootJoint
from nhf.parts.box import MountingBox
import nhf.utils import nhf.utils
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -46,21 +47,21 @@ class Mannequin:
return result.translate((0, self.torso_thickness / 2, 0)) return result.translate((0, self.torso_thickness / 2, 0))
BASE_POS_X = 60.0 BASE_POS_X = 70.0
BASE_POS_Y = 100.0 BASE_POS_Y = 100.0
@dataclass(kw_only=True) @dataclass(kw_only=True)
class Harness(Model): class Harness(Model):
thickness: float = 25.4 / 8 thickness: float = 25.4 / 8
width: float = 220.0 width: float = 220.0
height: float = 310.0 height: float = 304.8
fillet: float = 10.0 fillet: float = 10.0
wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [
("r1", BASE_POS_X + 10, BASE_POS_Y), ("r1", BASE_POS_X, BASE_POS_Y),
("l1", -BASE_POS_X - 10, BASE_POS_Y), ("l1", -BASE_POS_X, BASE_POS_Y),
("r2", BASE_POS_X + 10, 0), ("r2", BASE_POS_X, 0),
("l2", -BASE_POS_X - 10, 0), ("l2", -BASE_POS_X, 0),
("r3", BASE_POS_X, -BASE_POS_Y), ("r3", BASE_POS_X, -BASE_POS_Y),
("l3", -BASE_POS_X, -BASE_POS_Y), ("l3", -BASE_POS_X, -BASE_POS_Y),
]) ])
@ -72,9 +73,12 @@ class Harness(Model):
def __post_init__(self): def __post_init__(self):
super().__init__(name="harness") super().__init__(name="harness")
@submodel(name="root-joint") @submodel(name="bridge-pair-horizontal")
def submodel_root_joint(self) -> Model: def bridge_pair_horizontal(self) -> MountingBox:
return self.root_joint 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) @target(name="profile", kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch: def profile(self) -> Cq.Sketch:
@ -126,7 +130,7 @@ class Harness(Model):
conn = [(px + x, py + y) for px, py conn = [(px + x, py + y) for px, py
in self.root_joint.corner_pos()] in self.root_joint.corner_pos()]
for i, (px, py) in enumerate(conn): 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 return result
def add_root_joint_constraint( def add_root_joint_constraint(
@ -144,7 +148,6 @@ class Harness(Model):
harness = self.surface() harness = self.surface()
mannequin_z = self.mannequin.shoulder_to_waist * 0.6 mannequin_z = self.mannequin.shoulder_to_waist * 0.6
result = ( result = (
Cq.Assembly() Cq.Assembly()
.addS( .addS(
@ -159,6 +162,37 @@ class Harness(Model):
loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180)) loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180))
.constrain("mannequin", "Fixed") .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: if with_root_joint:
for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: for name in ["l1", "l2", "l3", "r1", "r2", "r3"]:
result.addS( result.addS(

View File

@ -4,6 +4,7 @@ from typing import Optional, Tuple
import cadquery as Cq import cadquery as Cq
from nhf import Material, Role from nhf import Material, Role
from nhf.build import Model, target, assembly from nhf.build import Model, target, assembly
from nhf.parts.box import MountingBox
from nhf.parts.springs import TorsionSpring from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.joints import TorsionJoint, HirthJoint
@ -88,6 +89,10 @@ class RootJoint(Model):
parent_corner_inset: float = 12 parent_corner_inset: float = 12
parent_mount_thickness: float = 25.4 / 16 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_dx: float = 17.0
child_corner_dz: float = 24.0 child_corner_dz: float = 24.0
@ -103,6 +108,8 @@ class RootJoint(Model):
def __post_init__(self): def __post_init__(self):
assert self.child_extra_thickness > 0.0 assert self.child_extra_thickness > 0.0
assert self.parent_thickness >= self.hex_nut.thickness 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]]: def corner_pos(self) -> list[tuple[int, int]]:
""" """
@ -111,9 +118,9 @@ class RootJoint(Model):
dx = self.parent_width / 2 - self.parent_corner_inset dx = self.parent_width / 2 - self.parent_corner_inset
return [ return [
(dx, dx), (dx, dx),
(dx, -dx),
(-dx, -dx),
(-dx, dx), (-dx, dx),
(-dx, -dx),
(dx, -dx),
] ]
@property @property
@ -127,6 +134,45 @@ class RootJoint(Model):
def base_to_surface_thickness(self) -> float: def base_to_surface_thickness(self) -> float:
return self.hirth_joint.joint_height + self.child_extra_thickness 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") @target(name="parent")
def parent(self): def parent(self):
""" """
@ -142,7 +188,6 @@ class RootJoint(Model):
width=self.parent_width, width=self.parent_width,
height=self.parent_thickness, height=self.parent_thickness,
centered=(True, True, False)) centered=(True, True, False))
.translate((0, 0, -self.parent_thickness))
.edges("|Z") .edges("|Z")
.fillet(self.parent_corner_fillet) .fillet(self.parent_corner_fillet)
.faces(">Z") .faces(">Z")
@ -153,6 +198,32 @@ class RootJoint(Model):
cboreDiameter=self.parent_corner_cbore_diam, cboreDiameter=self.parent_corner_cbore_diam,
cboreDepth=self.parent_corner_cbore_depth) 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 # Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.parent_thickness) plane = result.faces(">Z").workplane(offset=-self.parent_thickness)
@ -171,7 +242,7 @@ class RootJoint(Model):
.hole(diameter=self.axis_diam) .hole(diameter=self.axis_diam)
.cut(self.hex_nut.generate().translate((0, 0, -self.parent_thickness))) .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 return result
@target(name="child") @target(name="child")

View File

@ -142,6 +142,9 @@ class WingProfile(Model):
extra = 2 * self.panel_thickness if self.flip else 0 extra = 2 * self.panel_thickness if self.flip else 0
return self.s1_thickness - 2 * self.panel_thickness - extra 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") @submodel(name="shoulder-joint")
def submodel_shoulder_joint(self) -> Model: def submodel_shoulder_joint(self) -> Model:
return self.shoulder_joint 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=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( return MountingBox(
length=self.root_height, length=self.root_height,