cosplay: Touhou/Houjuu Nue #4
|
@ -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()
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
width=self.child_lip_width,
|
||||||
|
height=lip_thickness,
|
||||||
|
hole_loc=self.child_conn_hole_pos,
|
||||||
|
hole_diam=self.child_conn_hole_diam,
|
||||||
|
)
|
||||||
lip = (
|
lip = (
|
||||||
Cq.Workplane('XY')
|
lip
|
||||||
.box(lip_ext, lip_height, lip_thickness,
|
|
||||||
centered=(False, True, False))
|
|
||||||
.copyWorkplane(Cq.Workplane('XY'))
|
.copyWorkplane(Cq.Workplane('XY'))
|
||||||
.cylinder(radius=joint.radius_rider, height=lip_thickness,
|
.cylinder(
|
||||||
|
radius=joint.radius_rider,
|
||||||
|
height=lip_thickness,
|
||||||
centered=(True, True, False),
|
centered=(True, True, False),
|
||||||
combine='cut')
|
combine='cut')
|
||||||
.faces(">Z")
|
|
||||||
.workplane()
|
|
||||||
)
|
)
|
||||||
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")
|
||||||
|
|
|
@ -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
|
def assembly_s3(self) -> Cq.Assembly:
|
||||||
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 = (
|
result = (
|
||||||
Cq.Sketch()
|
Cq.Assembly()
|
||||||
.segment((0, 0), (0, self.shoulder_height))
|
.add(self.surface_s3(front=True), name="front",
|
||||||
.spline([
|
color=self.material_panel.color)
|
||||||
(0, self.shoulder_height),
|
.constrain("front", "Fixed")
|
||||||
((w - s_th * self.elbow_height) / 2, self.shoulder_height / 2 + (self.elbow_height * c_th - h) / 2 - bend),
|
.add(self.surface_s3(front=False), name="back",
|
||||||
(w - s_th * self.elbow_height, self.elbow_height * c_th - h),
|
color=self.material_panel.color)
|
||||||
])
|
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
|
||||||
.segment(
|
param=self.s1_thickness)
|
||||||
(w - s_th * self.elbow_height, self.elbow_height * c_th -h),
|
|
||||||
(w, -h),
|
|
||||||
)
|
)
|
||||||
.spline([
|
for t in ["wrist_bot", "wrist_top"]:
|
||||||
(0, 0),
|
is_top = t.endswith("_top")
|
||||||
(w / 2, -h / 2 - bend),
|
is_parent = True
|
||||||
(w, -h),
|
o = self.spacer_s3_wrist()
|
||||||
])
|
self._assembly_insert_spacer(
|
||||||
.assemble()
|
result,
|
||||||
|
o.generate(),
|
||||||
|
point_tag=t,
|
||||||
|
flipped=is_top != is_parent,
|
||||||
)
|
)
|
||||||
return result
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue