cosplay: Touhou/Houjuu Nue #4

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

110
nhf/parts/box.py Normal file
View File

@ -0,0 +1,110 @@
import cadquery as Cq
from dataclasses import dataclass, field
from typing import Tuple, Optional, Union
import nhf.utils
def box_with_centre_holes(
length: float,
width: float,
height: float,
hole_loc: list[float],
hole_diam: float = 6.0,
) -> Cq.Workplane:
"""
Creates a box with holes along the X axis, marked `conn0, conn1, ...`. The
box's y axis is centred
"""
result = (
Cq.Workplane('XY')
.box(length, width, height, centered=(False, True, False))
.faces(">Z")
.workplane()
)
plane = result
for i, x in enumerate(hole_loc):
result = result.moveTo(x, 0).hole(hole_diam)
plane.moveTo(x, 0).tagPlane(f"conn{i}")
return result
@dataclass
class Hole:
x: float
y: float = 0.0
diam: Optional[float] = None
tag: Optional[str] = None
@dataclass
class MountingBox:
"""
Create a box with marked holes
"""
length: float = 100.0
width: float = 60.0
thickness: float = 1.0
# List of (x, y), diam
holes: list[Hole] = field(default_factory=lambda: [
Hole(x=5, y=5, diam=3),
Hole(x=20, y=10, diam=5),
])
hole_diam: Optional[float] = None
centred: Tuple[bool, bool] = (False, True)
generate_side_tags: bool = True
def profile(self) -> Cq.Sketch:
bx, by = 0, 0
if not self.centred[0]:
bx = self.length / 2
if not self.centred[1]:
by = self.width / 2
result = (
Cq.Sketch()
.push([(bx, by)])
.rect(self.length, self.width)
)
for hole in self.holes:
diam = hole.diam if hole.diam else self.hole_diam
result.push([(hole.x, hole.y)]).circle(diam / 2, mode='s')
return result
def generate(self) -> Cq.Workplane:
"""
Creates box shape with markers
"""
result = (
Cq.Workplane('XY')
.placeSketch(self.profile())
.extrude(self.thickness)
)
plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness)
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)
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("right")
result.faces("<X").workplane(origin=result.vertices("<X and <Y and >Z").val().Center()).tagPlane("bot")
result.faces(">X").workplane(origin=result.vertices(">X and <Y and >Z").val().Center()).tagPlane("top")
result.faces(">Z").tag("dir")
return result
def marked_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.generate(), name="box")
)
for i in range(len(self.holes)):
result.markPlane(f"box?conn{i}")
if self.generate_side_tags:
(
result
.markPlane("box?left")
.markPlane("box?right")
.markPlane("box?dir")
.markPlane("box?top")
.markPlane("box?bot")
)
return result.solve()

View File

@ -105,6 +105,18 @@ class HirthJoint:
) )
return result return result
def add_constraints(self,
assembly: Cq.Assembly,
parent: str,
child: str,
angle: int = 0):
(
assembly
.constrain(f"{parent}?mate", f"{child}?mate", "Plane")
.constrain(f"{parent}?dir", f"{child}?dir",
"Axis", param=angle * self.tooth_angle)
)
def assembly(self, offset: int = 1): def assembly(self, offset: int = 1):
""" """
Generate an example assembly Generate an example assembly

View File

@ -62,7 +62,11 @@ class Parameters(Model):
)) ))
wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(
shoulder_height=100.0, shoulder_joint=MJ.ShoulderJoint(
height=100.0,
),
elbow_joint=MJ.ElbowJoint(),
wrist_joint=MJ.ElbowJoint(),
elbow_height=110.0, elbow_height=110.0,
)) ))
@ -70,14 +74,6 @@ class Parameters(Model):
wing_root_radius: float = 40 wing_root_radius: float = 40
wing_root_wall_thickness: float = 8 wing_root_wall_thickness: float = 8
shoulder_joint: MJ.ShoulderJoint = field(default_factory=lambda: MJ.ShoulderJoint(
shoulder_height=100.0,
))
elbow_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
))
wrist_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
))
""" """
Heights for various wing joints, where the numbers start from the first Heights for various wing joints, where the numbers start from the first
joint. joint.
@ -109,6 +105,7 @@ class Parameters(Model):
def __post_init__(self): def __post_init__(self):
super().__init__(name="houjuu-nue") super().__init__(name="houjuu-nue")
self.harness.hs_hirth_joint = self.hs_hirth_joint self.harness.hs_hirth_joint = self.hs_hirth_joint
self.wing_profile.base_joint = self.hs_hirth_joint
assert self.wing_root_radius > self.hs_hirth_joint.radius, \ assert self.wing_root_radius > self.hs_hirth_joint.radius, \
"Wing root must be large enough to accomodate joint" "Wing root must be large enough to accomodate joint"
assert self.wing_s1_shoulder_spacer_hole_dist > self.wing_s1_spacer_hole_diam, \ assert self.wing_s1_shoulder_spacer_hole_dist > self.wing_s1_spacer_hole_diam, \
@ -145,309 +142,20 @@ class Parameters(Model):
def harness_assembly(self) -> Cq.Assembly: def harness_assembly(self) -> Cq.Assembly:
return self.harness.assembly() return self.harness.assembly()
#@target(name="wing/joining-plate", kind=TargetKind.DXF)
#def joining_plate(self) -> Cq.Workplane:
# return self.wing_joining_plate.plate()
@target(name="wing/root")
def wing_root(self) -> Cq.Assembly:
"""
Generate the wing root which contains a Hirth joint at its base and a
rectangular opening on its side, with the necessary interfaces.
"""
return MW.wing_root(
joint=self.hs_hirth_joint,
shoulder_attach_dist=self.shoulder_joint.attach_dist,
shoulder_attach_diam=self.shoulder_joint.attach_diam,
wall_thickness=self.wing_root_wall_thickness,
conn_height=self.wing_profile.shoulder_height,
conn_thickness=self.wing_s0_thickness,
)
@target(name="wing/proto-shoulder-joint-parent", prototype=True) @target(name="wing/proto-shoulder-joint-parent", prototype=True)
def proto_shoulder_joint_parent(self): def proto_shoulder_joint_parent(self):
return self.shoulder_joint.torsion_joint.track() return self.wing_profile.shoulder_joint.torsion_joint.track()
@target(name="wing/proto-shoulder-joint-child", prototype=True) @target(name="wing/proto-shoulder-joint-child", prototype=True)
def proto_shoulder_joint_child(self): def proto_shoulder_joint_child(self):
return self.shoulder_joint.torsion_joint.rider() return self.wing_profile.shoulder_joint.torsion_joint.rider()
@assembly()
def shoulder_assembly(self):
return self.shoulder_joint.assembly(
wing_root_wall_thickness=self.wing_root_wall_thickness,
lip_height=self.wing_s1_thickness,
hole_dist=self.wing_s1_shoulder_spacer_hole_dist,
spacer_hole_diam=self.wing_s1_spacer_hole_diam,
)
@assembly()
def elbow_assembly(self):
return self.elbow_joint.assembly()
@assembly()
def wrist_assembly(self):
return self.wrist_joint.assembly()
@target(name="wing/s1-spacer", kind=TargetKind.DXF)
def wing_s1_spacer(self) -> Cq.Workplane:
result = (
Cq.Workplane('XZ')
.sketch()
.rect(self.wing_s1_spacer_width, self.wing_s1_thickness)
.finalize()
.extrude(self.wing_s1_spacer_thickness)
)
result.faces("<Z").tag("weld1")
result.faces(">Z").tag("weld2")
result.faces(">Y").tag("dir")
return result
@target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF)
def wing_s1_shoulder_spacer(self) -> Cq.Workplane:
"""
Creates a rectangular spacer. This could be cut from acrylic.
There are two holes on the top of the spacer. With the holes
"""
dx = self.wing_s1_shoulder_spacer_hole_dist
h = self.wing_s1_spacer_thickness
length = self.wing_s1_shoulder_spacer_width
hole_diam = self.wing_s1_spacer_hole_diam
assert dx + hole_diam < length / 2
result = (
Cq.Workplane('XY')
.sketch()
.rect(length, self.wing_s1_thickness)
.push([
(0, 0),
(dx, 0),
])
.circle(hole_diam / 2, mode='s')
.finalize()
.extrude(h)
)
# Tag the mating surfaces to be glued
result.faces("<Y").workplane().moveTo(length / 2, h).tagPlane("left")
result.faces(">Y").workplane().moveTo(-length / 2, h).tagPlane("right")
# Tag the directrix
result.faces(">Z").tag("dir")
# Tag the holes
plane = result.faces(">Z").workplane()
# Side closer to the parent is 0
plane.moveTo(dx, 0).tagPlane("conn0")
plane.tagPlane("conn1")
return result
def assembly_insert_shoulder_spacer(
self,
assembly,
spacer,
point_tag: str,
front_tag: str = "panel_front",
back_tag: str = "panel_back",
flipped: bool = False,
):
"""
For a child joint facing up, front panel should be on the right, back
panel on the left
"""
site_front, site_back = "right", "left"
if flipped:
site_front, site_back = site_back, site_front
angle = 0
(
assembly
.add(spacer,
name=f"{point_tag}_spacer",
color=self.material_bracket.color)
.constrain(f"{front_tag}?{point_tag}",
f"{point_tag}_spacer?{site_front}", "Plane")
.constrain(f"{back_tag}?{point_tag}",
f"{point_tag}_spacer?{site_back}", "Plane")
.constrain(f"{point_tag}_spacer?dir", f"{front_tag}?{point_tag}_dir",
"Axis", param=angle)
)
@target(name="wing/r1s1", kind=TargetKind.DXF)
def wing_r1s1_profile(self) -> Cq.Sketch:
"""
FIXME: Output individual segment profiles
"""
return self.wing_profile.profile()
def wing_r1s1_panel(self, front=True) -> Cq.Workplane:
return self.wing_profile.surface_s1(
thickness=self.panel_thickness,
shoulder_joint_child_height=self.shoulder_joint.child_height,
front=front,
)
def wing_r1s2_panel(self, front=True) -> Cq.Workplane:
return self.wing_profile.surface_s2(
thickness=self.panel_thickness,
front=front,
)
def wing_r1s3_panel(self, front=True) -> Cq.Workplane:
return self.wing_profile.surface_s3(
thickness=self.panel_thickness,
front=front,
)
@assembly()
def wing_r1s1_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.wing_r1s1_panel(front=True), name="panel_front",
color=self.material_panel.color)
.constrain("panel_front", "Fixed")
.add(self.wing_r1s1_panel(front=False), name="panel_back",
color=self.material_panel.color)
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
param=self.wing_s1_thickness)
)
for t in ["shoulder_bot", "shoulder_top", "elbow_bot", "elbow_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("shoulder")
self.assembly_insert_shoulder_spacer(
result,
self.wing_s1_shoulder_spacer(),
point_tag=t,
flipped=is_top == is_parent,
)
return result.solve()
@assembly()
def wing_r1s2_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.wing_r1s2_panel(front=True), name="panel_front",
color=self.material_panel.color)
.constrain("panel_front", "Fixed")
.add(self.wing_r1s2_panel(front=False), name="panel_back",
color=self.material_panel.color)
# FIXME: Use s2 thickness
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
param=self.wing_s1_thickness)
)
for t in ["elbow_bot", "elbow_top", "wrist_bot", "wrist_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("elbow")
self.assembly_insert_shoulder_spacer(
result,
self.wing_s1_shoulder_spacer(),
point_tag=t,
flipped=is_top == is_parent,
)
return result.solve()
@assembly()
def wing_r1s3_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.wing_r1s3_panel(front=True), name="panel_front",
color=self.material_panel.color)
.constrain("panel_front", "Fixed")
.add(self.wing_r1s3_panel(front=False), name="panel_back",
color=self.material_panel.color)
# FIXME: Use s2 thickness
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
param=self.wing_s1_thickness)
)
for t in ["wrist_bot", "wrist_top"]:
self.assembly_insert_shoulder_spacer(
result,
self.wing_s1_shoulder_spacer(),
point_tag=t
)
return result.solve()
@assembly() @assembly()
def wing_r1_assembly( def wing_r1_assembly(self) -> Cq.Assembly:
self, return self.wing_profile.assembly()
parts=["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"],
) -> Cq.Assembly:
result = (
Cq.Assembly()
)
if "s0" in parts:
(
result
.add(self.wing_root(), name="s0")
.constrain("s0/scaffold", "Fixed")
)
if "shoulder" in parts:
result.add(self.shoulder_assembly(), name="shoulder")
if "s0" in parts and "shoulder" in parts:
(
result
.constrain("s0/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane")
.constrain("s0/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane")
.constrain("s0/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane")
.constrain("s0/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane")
)
if "s1" in parts:
result.add(self.wing_r1s1_assembly(), name="s1")
if "s1" in parts and "shoulder" in parts:
(
result
.constrain("shoulder/child/lip_bot?conn0",
"s1/shoulder_bot_spacer?conn0",
"Plane")
.constrain("shoulder/child/lip_bot?conn1",
"s1/shoulder_bot_spacer?conn1",
"Plane")
.constrain("shoulder/child/lip_top?conn0",
"s1/shoulder_top_spacer?conn0",
"Plane")
.constrain("shoulder/child/lip_top?conn1",
"s1/shoulder_top_spacer?conn1",
"Plane")
)
if "elbow" in parts:
result.add(self.elbow_assembly(), name="elbow")
if "s2" in parts:
result.add(self.wing_r1s2_assembly(), name="s2")
if "s1" in parts and "elbow" in parts:
(
result
.constrain("elbow/parent_upper/top?conn1",
"s1/elbow_top_spacer?conn1",
"Plane")
.constrain("elbow/parent_upper/top?conn0",
"s1/elbow_top_spacer?conn0",
"Plane")
.constrain("elbow/parent_upper/bot?conn1",
"s1/elbow_bot_spacer?conn1",
"Plane")
.constrain("elbow/parent_upper/bot?conn0",
"s1/elbow_bot_spacer?conn0",
"Plane")
)
if "s2" in parts and "elbow" in parts:
(
result
.constrain("elbow/child/bot?conn0",
"s2/elbow_bot_spacer?conn0",
"Plane")
.constrain("elbow/child/bot?conn1",
"s2/elbow_bot_spacer?conn1",
"Plane")
.constrain("elbow/child/top?conn0",
"s2/elbow_top_spacer?conn0",
"Plane")
.constrain("elbow/child/top?conn1",
"s2/elbow_top_spacer?conn1",
"Plane")
)
if len(parts) > 1:
result.solve()
return result
@assembly() @assembly()
def wings_assembly(self) -> Cq.Assembly: def wings_harness_assembly(self) -> Cq.Assembly:
""" """
Assembly of harness with all the wings Assembly of harness with all the wings
""" """
@ -456,18 +164,14 @@ class Parameters(Model):
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0))) .add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
.add(self.wing_root(), name="w0_r1") .add(self.wing_r1_assembly(), name="wing_r1")
.add(self.wing_root(), name="w0_l1") .add(self.wing_r1_assembly(), name="wing_r2")
.constrain("harness/base", "Fixed") .add(self.wing_r1_assembly(), name="wing_r3")
.constrain("w0_r1/joint?mate", "harness/r1?mate", "Plane")
.constrain("w0_r1/joint?dir", "harness/r1?dir",
"Axis", param=7 * a_tooth)
.constrain("w0_l1/joint?mate", "harness/l1?mate", "Plane")
.constrain("w0_l1/joint?dir", "harness/l1?dir",
"Axis", param=-1 * a_tooth)
.solve()
) )
return result self.hs_hirth_joint.add_constraints(result, "harness/r1", "wing_r1/s0/hs", angle=9)
self.hs_hirth_joint.add_constraints(result, "harness/r2", "wing_r2/s0/hs", angle=8)
self.hs_hirth_joint.add_constraints(result, "harness/r3", "wing_r3/s0/hs", angle=7)
return result.solve()
@assembly(collision_check=False) @assembly(collision_check=False)
def trident_assembly(self) -> Cq.Assembly: def trident_assembly(self) -> Cq.Assembly:

View File

@ -5,6 +5,7 @@ from nhf import Role
from nhf.build import Model, target, assembly from nhf.build import Model, target, assembly
import nhf.parts.springs as springs import nhf.parts.springs as springs
from nhf.parts.joints import TorsionJoint from nhf.parts.joints import TorsionJoint
from nhf.parts.box import box_with_centre_holes
import nhf.utils import nhf.utils
TOL = 1e-6 TOL = 1e-6
@ -12,7 +13,7 @@ TOL = 1e-6
@dataclass @dataclass
class ShoulderJoint(Model): class ShoulderJoint(Model):
shoulder_height: float = 100.0 height: float = 100.0
torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=18, radius_track=18,
radius_rider=18, radius_rider=18,
@ -27,47 +28,67 @@ class ShoulderJoint(Model):
spring_thickness=1.3, spring_thickness=1.3,
spring_height=7.5, spring_height=7.5,
)) ))
# Two holes on each side (top and bottom) are used to attach the shoulder
# joint. This governs the distance between these two holes # On the parent side, drill vertical holes
attach_dist: float = 25
attach_diam: float = 8 parent_conn_hole_diam: float = 6.0
# Position of the holes relative
parent_conn_hole_pos: list[float] = field(default_factory=lambda: [20, 30])
parent_lip_length: float = 40.0
parent_lip_width: float = 20.0
parent_lip_thickness: float = 8.0
parent_lip_ext: float = 40.0
parent_lip_guard_height: float = 10.0
# Measured from centre of axle
child_lip_length: float = 45.0
child_lip_width: float = 20.0
child_conn_hole_diam: float = 6.0
# Measured from centre of axle
child_conn_hole_pos: list[float] = field(default_factory=lambda: [25, 35])
child_core_thickness: float = 3.0
@target(name="shoulder-joint/parent") @target(name="shoulder-joint/parent")
def parent(self, def parent(self,
wing_root_wall_thickness: float = 5.0) -> Cq.Workplane: root_wall_thickness: float = 25.4 / 16) -> Cq.Assembly:
joint = self.torsion_joint joint = self.torsion_joint
# Thickness of the lip connecting this joint to the wing root # Thickness of the lip connecting this joint to the wing root
lip_thickness = 10 dz = root_wall_thickness
lip_width = 25 assert self.parent_lip_width <= joint.radius_track * 2
lip_guard_ext = 40 assert self.parent_lip_ext > joint.radius_track
lip_guard_height = wing_root_wall_thickness + lip_thickness
assert lip_guard_ext > joint.radius_track
lip_guard = ( lip_guard = (
Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height) Cq.Solid.makeBox(
.located(Cq.Location((0, -lip_width/2 , 0))) self.parent_lip_ext,
.cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height)) self.parent_lip_width,
self.parent_lip_guard_height)
.located(Cq.Location((0, -self.parent_lip_width/2 , dz)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height + dz))
) )
lip = box_with_centre_holes(
length=self.parent_lip_length - dz,
width=self.parent_lip_width,
height=self.parent_lip_thickness,
hole_loc=[
self.height / 2 - dz - x
for x in self.parent_conn_hole_pos
],
hole_diam=self.parent_conn_hole_diam,
)
# Flip so the lip's holes point to -X
loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90)
# so they point to +X
loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180)
loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, dz))
result = ( result = (
joint.track() Cq.Assembly()
.union(lip_guard, tol=1e-6) .add(joint.track(), name="track")
.add(lip_guard, name="lip_guard")
# Extrude the handle .add(lip, name="lip", loc=loc_pos * loc_dir * loc_axis)
.copyWorkplane(Cq.Workplane(
'YZ', origin=Cq.Vector((88, 0, wing_root_wall_thickness))))
.rect(lip_width, lip_thickness, centered=(True, False))
.extrude("next")
# Connector holes on the lip
.copyWorkplane(Cq.Workplane(
'YX', origin=Cq.Vector((57, 0, wing_root_wall_thickness))))
.hole(self.attach_diam)
.moveTo(0, self.attach_dist)
.hole(self.attach_diam)
) )
result.moveTo(0, 0).tagPlane('conn0')
result.moveTo(0, self.attach_dist).tagPlane('conn1')
return result return result
@property @property
@ -77,32 +98,32 @@ class ShoulderJoint(Model):
of the shoulder joint. of the shoulder joint.
""" """
joint = self.torsion_joint joint = self.torsion_joint
return self.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height
@target(name="shoulder-joint/child") @target(name="shoulder-joint/child")
def child(self, def child(self) -> Cq.Assembly:
lip_height: float = 20.0,
hole_dist: float = 10.0,
spacer_hole_diam: float = 8.0) -> Cq.Assembly:
""" """
Creates the top/bottom shoulder child joint Creates the top/bottom shoulder child joint
""" """
joint = self.torsion_joint joint = self.torsion_joint
assert all(r > joint.radius_rider for r in self.child_conn_hole_pos)
assert all(r < self.child_lip_length for r in self.child_conn_hole_pos)
# Half of the height of the bridging cylinder # Half of the height of the bridging cylinder
dh = self.shoulder_height / 2 - joint.total_height dh = self.height / 2 - joint.total_height
core_start_angle = 30 core_start_angle = 30
core_end_angle1 = 90 core_end_angle1 = 90
core_end_angle2 = 180 core_end_angle2 = 180
core_thickness = 2
radius_core_inner = joint.radius_rider - self.child_core_thickness
core_profile1 = ( core_profile1 = (
Cq.Sketch() Cq.Sketch()
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle)
.segment((0, 0)) .segment((0, 0))
.close() .close()
.assemble() .assemble()
.circle(joint.radius_rider - core_thickness, mode='s') .circle(radius_core_inner, mode='s')
) )
core_profile2 = ( core_profile2 = (
Cq.Sketch() Cq.Sketch()
@ -110,7 +131,7 @@ class ShoulderJoint(Model):
.segment((0, 0)) .segment((0, 0))
.close() .close()
.assemble() .assemble()
.circle(joint.radius_rider - core_thickness, mode='s') .circle(radius_core_inner, mode='s')
) )
core = ( core = (
Cq.Workplane('XY') Cq.Workplane('XY')
@ -123,33 +144,24 @@ class ShoulderJoint(Model):
.extrude(dh * 2) .extrude(dh * 2)
.translate(Cq.Vector(0, 0, -dh)) .translate(Cq.Vector(0, 0, -dh))
) )
# Create the upper and lower lips assert self.child_lip_width / 2 <= joint.radius_rider
lip_thickness = joint.rider_disk_height lip_thickness = joint.rider_disk_height
lip_ext = 40 + joint.radius_rider lip = box_with_centre_holes(
assert lip_height / 2 <= joint.radius_rider length=self.child_lip_length,
lip = ( width=self.child_lip_width,
Cq.Workplane('XY') height=lip_thickness,
.box(lip_ext, lip_height, lip_thickness, hole_loc=self.child_conn_hole_pos,
centered=(False, True, False)) hole_diam=self.child_conn_hole_diam,
.copyWorkplane(Cq.Workplane('XY')) )
.cylinder(radius=joint.radius_rider, height=lip_thickness, lip = (
centered=(True, True, False), lip
combine='cut') .copyWorkplane(Cq.Workplane('XY'))
.faces(">Z") .cylinder(
.workplane() radius=joint.radius_rider,
height=lip_thickness,
centered=(True, True, False),
combine='cut')
) )
hole_x = lip_ext - hole_dist / 2
for i in range(2):
x = hole_x - i * hole_dist
lip = lip.moveTo(x, 0).hole(spacer_hole_diam)
for i in range(2):
x = hole_x - i * hole_dist
(
lip
.moveTo(x, 0)
.tagPlane(f"conn{1 - i}")
)
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
result = ( result = (
Cq.Assembly() Cq.Assembly()
@ -167,18 +179,12 @@ class ShoulderJoint(Model):
@assembly() @assembly()
def assembly(self, def assembly(self,
wing_root_wall_thickness: float = 5.0, wing_root_wall_thickness: float = 25.4/16,
lip_height: float = 5.0,
hole_dist: float = 10.0,
spacer_hole_diam: float = 8.0
) -> Cq.Assembly: ) -> Cq.Assembly:
directrix = 0 directrix = 0
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(self.child(lip_height=lip_height, .add(self.child(), name="child",
hole_dist=hole_dist,
spacer_hole_diam=spacer_hole_diam),
name="child",
color=Role.CHILD.color) color=Role.CHILD.color)
.constrain("child/core", "Fixed") .constrain("child/core", "Fixed")
.add(self.torsion_joint.spring(), name="spring_top", .add(self.torsion_joint.spring(), name="spring_top",
@ -194,12 +200,12 @@ class ShoulderJoint(Model):
) )
TorsionJoint.add_constraints(result, TorsionJoint.add_constraints(result,
rider="child/rider_top", rider="child/rider_top",
track="parent_top", track="parent_top/track",
spring="spring_top", spring="spring_top",
directrix=directrix) directrix=directrix)
TorsionJoint.add_constraints(result, TorsionJoint.add_constraints(result,
rider="child/rider_bot", rider="child/rider_bot",
track="parent_bot", track="parent_bot/track",
spring="spring_bot", spring="spring_bot",
directrix=directrix) directrix=directrix)
return result.solve() return result.solve()
@ -565,15 +571,31 @@ class ElbowJoint:
assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span
assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing
def child_hole_pos(self) -> list[float]:
"""
List of hole positions measured from axle
"""
dx = self.child_beam.hole_dist / 2
r = self.child_arm_radius
return [r - dx, r + dx]
def parent_hole_pos(self) -> list[float]:
"""
List of hole positions measured from axle
"""
dx = self.parent_beam.hole_dist / 2
r = self.parent_arm_radius
return [r - dx, r + dx]
def child_joint(self) -> Cq.Assembly: def child_joint(self) -> Cq.Assembly:
angle = -self.disk_joint.tongue_span / 2 angle = -self.disk_joint.tongue_span / 2
dz = self.disk_joint.disk_thickness / 2 dz = self.disk_joint.disk_thickness / 2
# We need to ensure the disk is on the "other" side so # We need to ensure the disk is on the "other" side so
flip = Cq.Location((0, 0, 0), (0, 0, 1), 180) flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180)
flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180)
result = ( result = (
self.child_beam.beam() self.child_beam.beam()
.add(self.disk_joint.disk(), name="disk", .add(self.disk_joint.disk(), name="disk",
loc=flip * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) loc=flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle))
#.constrain("disk", "Fixed") #.constrain("disk", "Fixed")
#.constrain("top", "Fixed") #.constrain("top", "Fixed")
#.constrain("bot", "Fixed") #.constrain("bot", "Fixed")

View File

@ -4,341 +4,46 @@ This file describes the shapes of the wing shells. The joints are defined in
""" """
import math import math
from enum import Enum from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Mapping, Tuple from typing import Mapping, Tuple, Optional
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.parts.box import box_with_centre_holes, MountingBox, Hole
from nhf.parts.joints import HirthJoint from nhf.parts.joints import HirthJoint
from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint
import nhf.utils import nhf.utils
def wing_root_profiles(
base_sweep=150,
wall_thickness=8,
base_radius=40,
middle_offset=30,
middle_height=80,
conn_thickness=40,
conn_height=100) -> tuple[Cq.Wire, Cq.Wire]:
assert base_sweep < 180
assert middle_offset > 0
theta = math.pi * base_sweep / 180
c, s = math.cos(theta), math.sin(theta)
c_1, s_1 = math.cos(theta * 0.75), math.sin(theta * 0.75)
c_2, s_2 = math.cos(theta / 2), math.sin(theta / 2)
r1 = base_radius
r2 = base_radius - wall_thickness
base = (
Cq.Sketch()
.arc(
(c * r1, s * r1),
(c_1 * r1, s_1 * r1),
(c_2 * r1, s_2 * r1),
)
.arc(
(c_2 * r1, s_2 * r1),
(r1, 0),
(c_2 * r1, -s_2 * r1),
)
.arc(
(c_2 * r1, -s_2 * r1),
(c_1 * r1, -s_1 * r1),
(c * r1, -s * r1),
)
.segment(
(c * r1, -s * r1),
(c * r2, -s * r2),
)
.arc(
(c * r2, -s * r2),
(c_1 * r2, -s_1 * r2),
(c_2 * r2, -s_2 * r2),
)
.arc(
(c_2 * r2, -s_2 * r2),
(r2, 0),
(c_2 * r2, s_2 * r2),
)
.arc(
(c_2 * r2, s_2 * r2),
(c_1 * r2, s_1 * r2),
(c * r2, s * r2),
)
.segment(
(c * r2, s * r2),
(c * r1, s * r1),
)
.assemble(tag="wire")
.wires().val()
)
assert isinstance(base, Cq.Wire)
# The interior sweep is given by theta, but the exterior sweep exceeds the
# interior sweep so the wall does not become thinner towards the edges.
# If the exterior sweep is theta', it has to satisfy
#
# sin(theta) * r2 + wall_thickness = sin(theta') * r1
x, y = conn_thickness / 2, middle_height / 2
t = wall_thickness
dx = middle_offset
middle = (
Cq.Sketch()
# Interior arc, top point
.arc(
(x - t, y - t),
(x - t + dx, 0),
(x - t, -y + t),
)
.segment(
(x - t, -y + t),
(-x, -y+t)
)
.segment((-x, -y))
.segment((x, -y))
# Outer arc, bottom point
.arc(
(x, -y),
(x + dx, 0),
(x, y),
)
.segment(
(x, y),
(-x, y)
)
.segment((-x, y-t))
#.segment((x2, a))
.close()
.assemble(tag="wire")
.wires().val()
)
assert isinstance(middle, Cq.Wire)
x, y = conn_thickness / 2, conn_height / 2
t = wall_thickness
tip = (
Cq.Sketch()
.segment((-x, y), (x, y))
.segment((x, -y))
.segment((-x, -y))
.segment((-x, -y+t))
.segment((x-t, -y+t))
.segment((x-t, y-t))
.segment((-x, y-t))
.close()
.assemble(tag="wire")
.wires().val()
)
return base, middle, tip
def wing_root(joint: HirthJoint,
bolt_diam: int = 12,
union_tol=1e-4,
shoulder_attach_diam=8,
shoulder_attach_dist=25,
conn_thickness=40,
conn_height=100,
wall_thickness=8) -> Cq.Assembly:
"""
Generate the contiguous components of the root wing segment
"""
tip_centre = Cq.Vector((-150, 0, -80))
attach_theta = math.radians(5)
c, s = math.cos(attach_theta), math.sin(attach_theta)
attach_points = [
(15, 4),
(15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s),
]
root_profile, middle_profile, tip_profile = wing_root_profiles(
conn_thickness=conn_thickness,
conn_height=conn_height,
wall_thickness=8,
)
middle_profile = middle_profile.located(Cq.Location(
(-40, 0, -40), (0, 1, 0), 30
))
antetip_profile = tip_profile.located(Cq.Location(
(-95, 0, -75), (0, 1, 0), 60
))
tip_profile = tip_profile.located(Cq.Location(
tip_centre, (0, 1, 0), 90
))
profiles = [
root_profile,
middle_profile,
antetip_profile,
tip_profile,
]
result = None
for p1, p2 in zip(profiles[:-1], profiles[1:]):
seg = (
Cq.Workplane('XY')
.add(p1)
.toPending()
.workplane() # This call is necessary
.add(p2)
.toPending()
.loft()
)
if result:
result = result.union(seg, tol=union_tol)
else:
result = seg
result = (
result
# Create connector holes
.copyWorkplane(
Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0)))
)
.pushPoints(attach_points)
.hole(shoulder_attach_diam)
)
# Generate attach point tags
for sign in [False, True]:
y = conn_height / 2 - wall_thickness
side = "bottom" if sign else "top"
y = -y if sign else y
plane = (
result
# Create connector holes
.copyWorkplane(
Cq.Workplane(side, origin=tip_centre +
Cq.Vector((0, y, 0)))
)
)
if side == "bottom":
side = "bot"
for i, (px, py) in enumerate(attach_points):
tag = f"conn_{side}{i}"
plane.moveTo(px, -py if side == "top" else py).tagPlane(tag, "-Z")
result.faces("<Z").tag("base")
result.faces(">X").tag("conn")
j = (
joint.generate(is_mated=True)
.faces("<Z")
.hole(bolt_diam)
)
color = Material.PLASTIC_PLA.color
result = (
Cq.Assembly()
.add(result, name="scaffold", color=color)
.add(j, name="joint", color=Role.CHILD.color,
loc=Cq.Location((0, 0, -joint.total_height)))
)
return result
@dataclass @dataclass
class WingRoot: class WingProfile(Model):
"""
Generator for the wing root profile and model name: str = "wing"
"""
base_joint: HirthJoint = field(default_factory=lambda: HirthJoint())
root_width: float = 80.0
hs_joint_corner_dx: float = 30.0
hs_joint_corner_hole_diam: float = 6.0
panel_thickness: float = 25.4 / 16 panel_thickness: float = 25.4 / 16
spacer_thickness: float = 25.4 / 8 spacer_thickness: float = 25.4 / 8
height: float = 100.0
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint())
shoulder_width: float = 30.0 shoulder_width: float = 30.0
root_width: float = 80.0 shoulder_tip_x: float = -200.0
shoulder_tip_y: float = 160.0
tip_x: float = -200.0 s1_thickness: float = 25.0
tip_y: float = 160.0
def outer_spline(self) -> list[Tuple[float, float]]:
"""
Generate outer wing shape spline
"""
def profile(self) -> Cq.Sketch:
sketch = (
Cq.Sketch()
.segment((-self.root_width, 0), (0, 0))
.spline([
(0, 0),
(-30.0, 80.0),
(self.tip_x, self.tip_y)
])
.segment(
(self.tip_x, self.tip_y),
(self.tip_x, self.tip_y - self.shoulder_width)
)
.segment(
(self.tip_x, self.tip_y - self.shoulder_width),
(-self.root_width, 0)
)
.assemble()
)
return sketch
def spacer(self) -> Cq.Workplane:
"""
Creates a rectangular spacer. This could be cut from acrylic.
There are two holes on the top of the spacer. With the holes
"""
length = self.height
width = 10.0
h = self.spacer_thickness
result = (
Cq.Workplane('XY')
.sketch()
.rect(length, width)
.finalize()
.extrude(h)
)
# Tag the mating surfaces to be glued
result.faces("<X").workplane().tagPlane("left")
result.faces(">X").workplane().tagPlane("right")
# Tag the directrix
result.faces(">Z").tag("dir")
return result
def surface(self, top: bool = False) -> Cq.Workplane:
tags = [
("shoulder", (self.tip_x, self.tip_y + 30), 0),
("base", (-self.root_width, 0), 90),
]
return nhf.utils.extrude_with_markers(
self.profile(),
self.panel_thickness,
tags,
reverse=not top,
)
def assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface(top=True), name="bot")
.add(self.surface(top=False), name="top")
.constrain("bot@faces@>Z", "top@faces@<Z", "Point",
param=self.height)
)
for t in ["shoulder", "base"]:
name = f"{t}_spacer"
(
result
.add(self.spacer(), name=name)
.constrain(f"{name}?left", f"bot?{t}", "Plane")
.constrain(f"{name}?right", f"top?{t}", "Plane")
.constrain(f"{name}?dir", f"top?{t}_dir", "Axis")
)
return result.solve()
@dataclass
class WingProfile:
shoulder_height: float = 100
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint())
elbow_height: float = 100 elbow_height: float = 100
elbow_x: float = 240 elbow_x: float = 240
elbow_y: float = 30 elbow_y: float = 30
# Tilt of elbow w.r.t. shoulder # Tilt of elbow w.r.t. shoulder
elbow_angle: float = 20 elbow_angle: float = 20
s2_thickness: float = 25.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint())
wrist_height: float = 70 wrist_height: float = 70
# Bottom point of the wrist # Bottom point of the wrist
wrist_x: float = 400 wrist_x: float = 400
@ -347,6 +52,8 @@ class WingProfile:
# Tile of wrist w.r.t. shoulder # Tile of wrist w.r.t. shoulder
wrist_angle: float = 40 wrist_angle: float = 40
s3_thickness: float = 25.0
# Extends from the wrist to the tip of the arrow # Extends from the wrist to the tip of the arrow
arrow_height: float = 300 arrow_height: float = 300
arrow_angle: float = 7 arrow_angle: float = 7
@ -356,7 +63,11 @@ class WingProfile:
ring_y: float = 20 ring_y: float = 20
ring_radius_inner: float = 22 ring_radius_inner: float = 22
material_panel: Material = Material.ACRYLIC_TRANSPARENT
material_bracket: Material = Material.ACRYLIC_TRANSPARENT
def __post_init__(self): def __post_init__(self):
super().__init__(name=self.name)
assert self.ring_radius > self.ring_radius_inner assert self.ring_radius > self.ring_radius_inner
self.elbow_theta = math.radians(self.elbow_angle) self.elbow_theta = math.radians(self.elbow_angle)
@ -377,6 +88,145 @@ class WingProfile:
self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y
self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y
@property
def root_height(self) -> float:
return self.shoulder_joint.height
def profile_s0(self) -> Cq.Sketch:
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
sketch = (
Cq.Sketch()
.segment((-self.root_width, 0), (0, 0))
.spline([
(0, 0),
(-30.0, 80.0),
(tip_x, tip_y)
])
.segment(
(tip_x, tip_y),
(tip_x, tip_y - self.shoulder_width)
)
.segment(
(tip_x, tip_y - self.shoulder_width),
(-self.root_width, 0)
)
.assemble()
)
return sketch
def spacer_s0_shoulder(self) -> MountingBox:
"""
Should be cut
"""
holes = [
hole
for i, x in enumerate(self.shoulder_joint.parent_conn_hole_pos)
for hole in [
Hole(x=x, tag=f"conn_top{i}"),
Hole(x=-x, tag=f"conn_bot{i}"),
]
]
return MountingBox(
length=self.shoulder_joint.height,
width=self.shoulder_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.shoulder_joint.parent_conn_hole_diam,
centred=(True, True),
)
def spacer_s0_base(self) -> MountingBox:
"""
Should be cut
"""
dx = self.hs_joint_corner_dx
holes = [
Hole(x=-dx, y=-dx),
Hole(x=dx, y=-dx),
Hole(x=dx, y=dx),
Hole(x=-dx, y=dx),
]
return MountingBox(
length=self.root_height,
width=self.root_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.hs_joint_corner_hole_diam,
centred=(True, True),
)
def surface_s0(self, top: bool = False) -> Cq.Workplane:
tags = [
("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0),
("base", (0, 0), 90),
]
return nhf.utils.extrude_with_markers(
self.profile_s0(),
self.panel_thickness,
tags,
reverse=not top,
)
def assembly_s0(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s0(top=True), name="bot", color=self.material_panel.color)
.add(self.surface_s0(top=False), name="top", color=self.material_panel.color)
.constrain("bot@faces@>Z", "top@faces@<Z", "Point",
param=self.shoulder_joint.height)
)
for o, tag in [
(self.spacer_s0_shoulder().generate(), "shoulder"),
(self.spacer_s0_base().generate(), "base")
]:
(
result
.add(o, name=tag, color=self.material_bracket.color)
.constrain(f"{tag}?bot", f"bot?{tag}", "Plane")
.constrain(f"{tag}?top", f"top?{tag}", "Plane")
.constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis")
)
hirth = self.base_joint.generate()
(
result
.add(hirth, name="hs", color=Role.CHILD.color)
.constrain("hs@faces@<Z", "base?dir", "Plane")
)
return result.solve()
### s1, s2, s3 ###
def _assembly_insert_spacer(
self,
a: Cq.Assembly,
spacer: Cq.Workplane,
point_tag: str,
front_tag: str = "front",
back_tag: str = "back",
flipped: bool = False,
):
"""
For a child joint facing up, front panel should be on the right, back
panel on the left
"""
site_front, site_back = "right", "left"
if flipped:
site_front, site_back = site_back, site_front
angle = 0
(
a
.add(spacer,
name=point_tag,
color=self.material_bracket.color)
.constrain(f"{front_tag}?{point_tag}",
f"{point_tag}?{site_front}", "Plane")
.constrain(f"{back_tag}?{point_tag}",
f"{point_tag}?{site_back}", "Plane")
.constrain(f"{point_tag}?dir", f"{front_tag}?{point_tag}_dir",
"Axis", param=angle)
)
@property @property
def ring_radius(self) -> float: def ring_radius(self) -> float:
dx = self.ring_x dx = self.ring_x
@ -401,10 +251,10 @@ class WingProfile:
Cq.Sketch() Cq.Sketch()
.segment( .segment(
(0, 0), (0, 0),
(0, self.shoulder_height), (0, self.shoulder_joint.height),
tag="shoulder") tag="shoulder")
.arc( .arc(
(0, self.shoulder_height), (0, self.shoulder_joint.height),
(self.elbow_top_x, self.elbow_top_y), (self.elbow_top_x, self.elbow_top_y),
(self.wrist_top_x, self.wrist_top_y), (self.wrist_top_x, self.wrist_top_y),
tag="s1_top") tag="s1_top")
@ -475,31 +325,74 @@ class WingProfile:
) )
return profile return profile
def surface_s1(self, def surface_s1(self,
thickness: float = 25.4/16, shoulder_mount_inset: float = 0,
shoulder_mount_inset: float = 20, elbow_mount_inset: float = 0,
shoulder_joint_child_height: float = 80,
elbow_mount_inset: float = 20,
elbow_joint_parent_height: float = 60,
front: bool = True) -> Cq.Workplane: front: bool = True) -> Cq.Workplane:
assert shoulder_joint_child_height < self.shoulder_height shoulder_h = self.shoulder_joint.child_height
assert elbow_joint_parent_height < self.elbow_height h = (self.shoulder_joint.height - shoulder_h) / 2
h = (self.shoulder_height - shoulder_joint_child_height) / 2
tags_shoulder = [ tags_shoulder = [
("shoulder_bot", (shoulder_mount_inset, h), 90), ("shoulder_bot", (shoulder_mount_inset, h), 90),
("shoulder_top", (shoulder_mount_inset, h + shoulder_joint_child_height), 270), ("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270),
] ]
h = (self.elbow_height - elbow_joint_parent_height) / 2 elbow_h = self.elbow_joint.parent_beam.total_height
h = (self.elbow_height - elbow_h) / 2
tags_elbow = [ tags_elbow = [
("elbow_bot", ("elbow_bot",
self.elbow_to_abs(-elbow_mount_inset, h), self.elbow_to_abs(-elbow_mount_inset, h),
self.elbow_angle + 90), self.elbow_angle + 90),
("elbow_top", ("elbow_top",
self.elbow_to_abs(-elbow_mount_inset, h + elbow_joint_parent_height), self.elbow_to_abs(-elbow_mount_inset, h + elbow_h),
self.elbow_angle + 270), self.elbow_angle + 270),
] ]
profile = self.profile_s1() profile = self.profile_s1()
tags = tags_shoulder + tags_elbow tags = tags_shoulder + tags_elbow
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
def spacer_s1_shoulder(self) -> MountingBox:
holes = [
Hole(x)
for x in self.shoulder_joint.child_conn_hole_pos
]
return MountingBox(
length=50.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.shoulder_joint.child_conn_hole_diam,
)
def spacer_s1_elbow(self) -> MountingBox:
holes = [
Hole(x)
for x in self.elbow_joint.parent_hole_pos()
]
return MountingBox(
length=70.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.elbow_joint.hole_diam,
)
def assembly_s1(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s1(front=True), name="front",
color=self.material_panel.color)
.constrain("front", "Fixed")
.add(self.surface_s1(front=False), name="back",
color=self.material_panel.color)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
for t in ["shoulder_bot", "shoulder_top", "elbow_bot", "elbow_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("shoulder")
o = self.spacer_s1_shoulder().generate() if is_parent else self.spacer_s1_elbow().generate()
self._assembly_insert_spacer(
result,
o,
point_tag=t,
flipped=is_top != is_parent,
)
return result.solve()
def profile_s2(self) -> Cq.Sketch: def profile_s2(self) -> Cq.Sketch:
@ -513,33 +406,78 @@ class WingProfile:
return profile return profile
def surface_s2(self, def surface_s2(self,
thickness: float = 25.4/16, thickness: float = 25.4/16,
elbow_mount_inset: float = 20, elbow_mount_inset: float = 0,
elbow_joint_child_height: float = 80, wrist_mount_inset: float = 0,
wrist_mount_inset: float = 20,
wrist_joint_parent_height: float = 60,
front: bool = True) -> Cq.Workplane: front: bool = True) -> Cq.Workplane:
assert elbow_joint_child_height < self.elbow_height elbow_h = self.elbow_joint.child_beam.total_height
h = (self.elbow_height - elbow_joint_child_height) / 2 h = (self.elbow_height - elbow_h) / 2
tags_elbow = [ tags_elbow = [
("elbow_bot", ("elbow_bot",
self.elbow_to_abs(elbow_mount_inset, h), self.elbow_to_abs(elbow_mount_inset, h),
self.elbow_angle + 90), self.elbow_angle + 90),
("elbow_top", ("elbow_top",
self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height), self.elbow_to_abs(elbow_mount_inset, h + elbow_h),
self.elbow_angle - 90), self.elbow_angle - 90),
] ]
h = (self.wrist_height - wrist_joint_parent_height) / 2 wrist_h = self.wrist_joint.parent_beam.total_height
h = (self.wrist_height - wrist_h) / 2
tags_wrist = [ tags_wrist = [
("wrist_bot", ("wrist_bot",
self.wrist_to_abs(-wrist_mount_inset, h), self.wrist_to_abs(-wrist_mount_inset, h),
self.wrist_angle + 90), self.wrist_angle + 90),
("wrist_top", ("wrist_top",
self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height), self.wrist_to_abs(-wrist_mount_inset, h + wrist_h),
self.wrist_angle - 90), self.wrist_angle - 90),
] ]
profile = self.profile_s2() profile = self.profile_s2()
tags = tags_elbow + tags_wrist tags = tags_elbow + tags_wrist
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
def spacer_s2_elbow(self) -> MountingBox:
holes = [
Hole(x)
for x in self.elbow_joint.child_hole_pos()
]
return MountingBox(
length=50.0, # FIXME: magic
width=self.s2_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.elbow_joint.hole_diam,
)
def spacer_s2_wrist(self) -> MountingBox:
holes = [
Hole(x)
for x in self.wrist_joint.parent_hole_pos()
]
return MountingBox(
length=70.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.wrist_joint.hole_diam,
)
def assembly_s2(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s2(front=True), name="front",
color=self.material_panel.color)
.constrain("front", "Fixed")
.add(self.surface_s2(front=False), name="back",
color=self.material_panel.color)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
for t in ["elbow_bot", "elbow_top", "wrist_bot", "wrist_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("elbow")
o = self.spacer_s2_elbow() if is_parent else self.spacer_s2_wrist()
self._assembly_insert_spacer(
result,
o.generate(),
point_tag=t,
flipped=is_top != is_parent,
)
return result.solve()
def profile_s3(self) -> Cq.Sketch: def profile_s3(self) -> Cq.Sketch:
profile = ( profile = (
@ -549,61 +487,127 @@ class WingProfile:
) )
return profile return profile
def surface_s3(self, def surface_s3(self,
thickness: float = 25.4/16,
wrist_mount_inset: float = 20,
wrist_joint_child_height: float = 80,
front: bool = True) -> Cq.Workplane: front: bool = True) -> Cq.Workplane:
assert wrist_joint_child_height < self.wrist_height wrist_mount_inset = 0
h = (self.wrist_height - wrist_joint_child_height) / 2 wrist_h = self.wrist_joint.child_beam.total_height
h = (self.wrist_height - wrist_h) / 2
tags = [ tags = [
("wrist_bot", ("wrist_bot",
self.elbow_to_abs(wrist_mount_inset, h), self.wrist_to_abs(wrist_mount_inset, h),
self.elbow_angle + 90), self.wrist_angle + 90),
("wrist_top", ("wrist_top",
self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), self.wrist_to_abs(wrist_mount_inset, h + wrist_h),
self.elbow_angle - 90), self.wrist_angle - 90),
] ]
profile = self.profile_s3() profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
def spacer_s3_wrist(self) -> MountingBox:
holes = [
def wing_r1s1_profile(self) -> Cq.Sketch: Hole(x)
""" for x in self.wrist_joint.child_hole_pos()
Generates the first wing segment profile, with the wing root pointing in ]
the positive x axis. return MountingBox(
""" length=70.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
w = 270 holes=holes,
# Depression of the wing middle, measured hole_diam=self.wrist_joint.hole_diam
h = 0
# spline curve easing extension
theta = math.radians(30)
c_th, s_th = math.cos(theta), math.sin(theta)
bend = 30
ext = 40
ext_dh = -5
assert ext * 2 < w
factor = 0.7
result = (
Cq.Sketch()
.segment((0, 0), (0, self.shoulder_height))
.spline([
(0, self.shoulder_height),
((w - s_th * self.elbow_height) / 2, self.shoulder_height / 2 + (self.elbow_height * c_th - h) / 2 - bend),
(w - s_th * self.elbow_height, self.elbow_height * c_th - h),
])
.segment(
(w - s_th * self.elbow_height, self.elbow_height * c_th -h),
(w, -h),
)
.spline([
(0, 0),
(w / 2, -h / 2 - bend),
(w, -h),
])
.assemble()
) )
return result def assembly_s3(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s3(front=True), name="front",
color=self.material_panel.color)
.constrain("front", "Fixed")
.add(self.surface_s3(front=False), name="back",
color=self.material_panel.color)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
for t in ["wrist_bot", "wrist_top"]:
is_top = t.endswith("_top")
is_parent = True
o = self.spacer_s3_wrist()
self._assembly_insert_spacer(
result,
o.generate(),
point_tag=t,
flipped=is_top != is_parent,
)
return result.solve()
def assembly(self,
parts: Optional[list[str]] = None
) -> Cq.Assembly():
if parts is None:
parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
result = (
Cq.Assembly()
)
if "s0" in parts:
result.add(self.assembly_s0(), name="s0")
if "shoulder" in parts:
result.add(self.shoulder_joint.assembly(), name="shoulder")
if "s0" in parts and "shoulder" in parts:
(
result
.constrain("s0/shoulder?conn_top0", "shoulder/parent_top/lip?conn0", "Plane")
.constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane")
.constrain("s0/shoulder?conn_bot0", "shoulder/parent_bot/lip?conn0", "Plane")
.constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane")
)
if "s1" in parts:
result.add(self.assembly_s1(), name="s1")
if "s1" in parts and "shoulder" in parts:
(
result
.constrain("s1/shoulder_top?conn0", "shoulder/child/lip_top?conn0", "Plane")
.constrain("s1/shoulder_top?conn1", "shoulder/child/lip_top?conn1", "Plane")
.constrain("s1/shoulder_bot?conn0", "shoulder/child/lip_bot?conn0", "Plane")
.constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane")
)
if "elbow" in parts:
result.add(self.elbow_joint.assembly(), name="elbow")
if "s1" in parts and "elbow" in parts:
(
result
.constrain("s1/elbow_top?conn0", "elbow/parent_upper/top?conn0", "Plane")
.constrain("s1/elbow_top?conn1", "elbow/parent_upper/top?conn1", "Plane")
.constrain("s1/elbow_bot?conn0", "elbow/parent_upper/bot?conn0", "Plane")
.constrain("s1/elbow_bot?conn1", "elbow/parent_upper/bot?conn1", "Plane")
)
if "s2" in parts:
result.add(self.assembly_s2(), name="s2")
if "s2" in parts and "elbow" in parts:
(
result
.constrain("s2/elbow_top?conn0", "elbow/child/top?conn0", "Plane")
.constrain("s2/elbow_top?conn1", "elbow/child/top?conn1", "Plane")
.constrain("s2/elbow_bot?conn0", "elbow/child/bot?conn0", "Plane")
.constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane")
)
if "wrist" in parts:
result.add(self.wrist_joint.assembly(), name="wrist")
if "s2" in parts and "wrist" in parts:
(
result
.constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane")
.constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane")
.constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane")
.constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane")
)
if "s3" in parts:
result.add(self.assembly_s3(), name="s3")
if "s3" in parts and "wrist" in parts:
(
result
.constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane")
.constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane")
.constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane")
.constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane")
)
return result.solve()