refactor: Move wings to its own class with joints
This commit is contained in:
parent
bc8cda2eec
commit
027eec7264
|
@ -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
|
||||
|
||||
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):
|
||||
"""
|
||||
Generate an example assembly
|
||||
|
|
|
@ -62,7 +62,11 @@ class Parameters(Model):
|
|||
))
|
||||
|
||||
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,
|
||||
))
|
||||
|
||||
|
@ -70,14 +74,6 @@ class Parameters(Model):
|
|||
wing_root_radius: float = 40
|
||||
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
|
||||
joint.
|
||||
|
@ -109,6 +105,7 @@ class Parameters(Model):
|
|||
def __post_init__(self):
|
||||
super().__init__(name="houjuu-nue")
|
||||
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, \
|
||||
"Wing root must be large enough to accomodate joint"
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
def proto_shoulder_joint_child(self):
|
||||
return self.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()
|
||||
return self.wing_profile.shoulder_joint.torsion_joint.rider()
|
||||
|
||||
|
||||
@assembly()
|
||||
def wing_r1_assembly(
|
||||
self,
|
||||
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
|
||||
def wing_r1_assembly(self) -> Cq.Assembly:
|
||||
return self.wing_profile.assembly()
|
||||
|
||||
@assembly()
|
||||
def wings_assembly(self) -> Cq.Assembly:
|
||||
def wings_harness_assembly(self) -> Cq.Assembly:
|
||||
"""
|
||||
Assembly of harness with all the wings
|
||||
"""
|
||||
|
@ -456,18 +164,14 @@ class Parameters(Model):
|
|||
result = (
|
||||
Cq.Assembly()
|
||||
.add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
|
||||
.add(self.wing_root(), name="w0_r1")
|
||||
.add(self.wing_root(), name="w0_l1")
|
||||
.constrain("harness/base", "Fixed")
|
||||
.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()
|
||||
.add(self.wing_r1_assembly(), name="wing_r1")
|
||||
.add(self.wing_r1_assembly(), name="wing_r2")
|
||||
.add(self.wing_r1_assembly(), name="wing_r3")
|
||||
)
|
||||
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)
|
||||
def trident_assembly(self) -> Cq.Assembly:
|
||||
|
|
|
@ -5,6 +5,7 @@ from nhf import Role
|
|||
from nhf.build import Model, target, assembly
|
||||
import nhf.parts.springs as springs
|
||||
from nhf.parts.joints import TorsionJoint
|
||||
from nhf.parts.box import box_with_centre_holes
|
||||
import nhf.utils
|
||||
|
||||
TOL = 1e-6
|
||||
|
@ -12,7 +13,7 @@ TOL = 1e-6
|
|||
@dataclass
|
||||
class ShoulderJoint(Model):
|
||||
|
||||
shoulder_height: float = 100.0
|
||||
height: float = 100.0
|
||||
torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
|
||||
radius_track=18,
|
||||
radius_rider=18,
|
||||
|
@ -27,47 +28,67 @@ class ShoulderJoint(Model):
|
|||
spring_thickness=1.3,
|
||||
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
|
||||
attach_dist: float = 25
|
||||
attach_diam: float = 8
|
||||
|
||||
# On the parent side, drill vertical holes
|
||||
|
||||
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")
|
||||
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
|
||||
# Thickness of the lip connecting this joint to the wing root
|
||||
lip_thickness = 10
|
||||
lip_width = 25
|
||||
lip_guard_ext = 40
|
||||
lip_guard_height = wing_root_wall_thickness + lip_thickness
|
||||
assert lip_guard_ext > joint.radius_track
|
||||
dz = root_wall_thickness
|
||||
assert self.parent_lip_width <= joint.radius_track * 2
|
||||
assert self.parent_lip_ext > joint.radius_track
|
||||
|
||||
lip_guard = (
|
||||
Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height)
|
||||
.located(Cq.Location((0, -lip_width/2 , 0)))
|
||||
.cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height))
|
||||
Cq.Solid.makeBox(
|
||||
self.parent_lip_ext,
|
||||
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 = (
|
||||
joint.track()
|
||||
.union(lip_guard, tol=1e-6)
|
||||
|
||||
# Extrude the handle
|
||||
.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)
|
||||
Cq.Assembly()
|
||||
.add(joint.track(), name="track")
|
||||
.add(lip_guard, name="lip_guard")
|
||||
.add(lip, name="lip", loc=loc_pos * loc_dir * loc_axis)
|
||||
)
|
||||
result.moveTo(0, 0).tagPlane('conn0')
|
||||
result.moveTo(0, self.attach_dist).tagPlane('conn1')
|
||||
return result
|
||||
|
||||
@property
|
||||
|
@ -77,32 +98,32 @@ class ShoulderJoint(Model):
|
|||
of the shoulder 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")
|
||||
def child(self,
|
||||
lip_height: float = 20.0,
|
||||
hole_dist: float = 10.0,
|
||||
spacer_hole_diam: float = 8.0) -> Cq.Assembly:
|
||||
def child(self) -> Cq.Assembly:
|
||||
"""
|
||||
Creates the top/bottom shoulder child 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
|
||||
dh = self.shoulder_height / 2 - joint.total_height
|
||||
dh = self.height / 2 - joint.total_height
|
||||
core_start_angle = 30
|
||||
core_end_angle1 = 90
|
||||
core_end_angle2 = 180
|
||||
core_thickness = 2
|
||||
|
||||
radius_core_inner = joint.radius_rider - self.child_core_thickness
|
||||
core_profile1 = (
|
||||
Cq.Sketch()
|
||||
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle)
|
||||
.segment((0, 0))
|
||||
.close()
|
||||
.assemble()
|
||||
.circle(joint.radius_rider - core_thickness, mode='s')
|
||||
.circle(radius_core_inner, mode='s')
|
||||
)
|
||||
core_profile2 = (
|
||||
Cq.Sketch()
|
||||
|
@ -110,7 +131,7 @@ class ShoulderJoint(Model):
|
|||
.segment((0, 0))
|
||||
.close()
|
||||
.assemble()
|
||||
.circle(joint.radius_rider - core_thickness, mode='s')
|
||||
.circle(radius_core_inner, mode='s')
|
||||
)
|
||||
core = (
|
||||
Cq.Workplane('XY')
|
||||
|
@ -123,33 +144,24 @@ class ShoulderJoint(Model):
|
|||
.extrude(dh * 2)
|
||||
.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_ext = 40 + joint.radius_rider
|
||||
assert lip_height / 2 <= joint.radius_rider
|
||||
lip = box_with_centre_holes(
|
||||
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 = (
|
||||
Cq.Workplane('XY')
|
||||
.box(lip_ext, lip_height, lip_thickness,
|
||||
centered=(False, True, False))
|
||||
lip
|
||||
.copyWorkplane(Cq.Workplane('XY'))
|
||||
.cylinder(radius=joint.radius_rider, height=lip_thickness,
|
||||
.cylinder(
|
||||
radius=joint.radius_rider,
|
||||
height=lip_thickness,
|
||||
centered=(True, True, False),
|
||||
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)
|
||||
result = (
|
||||
Cq.Assembly()
|
||||
|
@ -167,18 +179,12 @@ class ShoulderJoint(Model):
|
|||
|
||||
@assembly()
|
||||
def assembly(self,
|
||||
wing_root_wall_thickness: float = 5.0,
|
||||
lip_height: float = 5.0,
|
||||
hole_dist: float = 10.0,
|
||||
spacer_hole_diam: float = 8.0
|
||||
wing_root_wall_thickness: float = 25.4/16,
|
||||
) -> Cq.Assembly:
|
||||
directrix = 0
|
||||
result = (
|
||||
Cq.Assembly()
|
||||
.add(self.child(lip_height=lip_height,
|
||||
hole_dist=hole_dist,
|
||||
spacer_hole_diam=spacer_hole_diam),
|
||||
name="child",
|
||||
.add(self.child(), name="child",
|
||||
color=Role.CHILD.color)
|
||||
.constrain("child/core", "Fixed")
|
||||
.add(self.torsion_joint.spring(), name="spring_top",
|
||||
|
@ -194,12 +200,12 @@ class ShoulderJoint(Model):
|
|||
)
|
||||
TorsionJoint.add_constraints(result,
|
||||
rider="child/rider_top",
|
||||
track="parent_top",
|
||||
track="parent_top/track",
|
||||
spring="spring_top",
|
||||
directrix=directrix)
|
||||
TorsionJoint.add_constraints(result,
|
||||
rider="child/rider_bot",
|
||||
track="parent_bot",
|
||||
track="parent_bot/track",
|
||||
spring="spring_bot",
|
||||
directrix=directrix)
|
||||
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.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:
|
||||
angle = -self.disk_joint.tongue_span / 2
|
||||
dz = self.disk_joint.disk_thickness / 2
|
||||
# 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 = (
|
||||
self.child_beam.beam()
|
||||
.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("top", "Fixed")
|
||||
#.constrain("bot", "Fixed")
|
||||
|
|
|
@ -4,341 +4,46 @@ This file describes the shapes of the wing shells. The joints are defined in
|
|||
"""
|
||||
import math
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import Mapping, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Mapping, Tuple, Optional
|
||||
import cadquery as Cq
|
||||
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.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint
|
||||
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
|
||||
class WingRoot:
|
||||
"""
|
||||
Generator for the wing root profile and model
|
||||
"""
|
||||
class WingProfile(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
|
||||
spacer_thickness: float = 25.4 / 8
|
||||
height: float = 100.0
|
||||
|
||||
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint())
|
||||
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
|
||||
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
|
||||
s1_thickness: float = 25.0
|
||||
|
||||
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint())
|
||||
elbow_height: float = 100
|
||||
elbow_x: float = 240
|
||||
elbow_y: float = 30
|
||||
# Tilt of elbow w.r.t. shoulder
|
||||
elbow_angle: float = 20
|
||||
|
||||
s2_thickness: float = 25.0
|
||||
|
||||
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint())
|
||||
wrist_height: float = 70
|
||||
# Bottom point of the wrist
|
||||
wrist_x: float = 400
|
||||
|
@ -347,6 +52,8 @@ class WingProfile:
|
|||
# Tile of wrist w.r.t. shoulder
|
||||
wrist_angle: float = 40
|
||||
|
||||
s3_thickness: float = 25.0
|
||||
|
||||
# Extends from the wrist to the tip of the arrow
|
||||
arrow_height: float = 300
|
||||
arrow_angle: float = 7
|
||||
|
@ -356,7 +63,11 @@ class WingProfile:
|
|||
ring_y: float = 20
|
||||
ring_radius_inner: float = 22
|
||||
|
||||
material_panel: Material = Material.ACRYLIC_TRANSPARENT
|
||||
material_bracket: Material = Material.ACRYLIC_TRANSPARENT
|
||||
|
||||
def __post_init__(self):
|
||||
super().__init__(name=self.name)
|
||||
assert self.ring_radius > self.ring_radius_inner
|
||||
|
||||
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_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
|
||||
def ring_radius(self) -> float:
|
||||
dx = self.ring_x
|
||||
|
@ -401,10 +251,10 @@ class WingProfile:
|
|||
Cq.Sketch()
|
||||
.segment(
|
||||
(0, 0),
|
||||
(0, self.shoulder_height),
|
||||
(0, self.shoulder_joint.height),
|
||||
tag="shoulder")
|
||||
.arc(
|
||||
(0, self.shoulder_height),
|
||||
(0, self.shoulder_joint.height),
|
||||
(self.elbow_top_x, self.elbow_top_y),
|
||||
(self.wrist_top_x, self.wrist_top_y),
|
||||
tag="s1_top")
|
||||
|
@ -475,31 +325,74 @@ class WingProfile:
|
|||
)
|
||||
return profile
|
||||
def surface_s1(self,
|
||||
thickness: float = 25.4/16,
|
||||
shoulder_mount_inset: float = 20,
|
||||
shoulder_joint_child_height: float = 80,
|
||||
elbow_mount_inset: float = 20,
|
||||
elbow_joint_parent_height: float = 60,
|
||||
shoulder_mount_inset: float = 0,
|
||||
elbow_mount_inset: float = 0,
|
||||
front: bool = True) -> Cq.Workplane:
|
||||
assert shoulder_joint_child_height < self.shoulder_height
|
||||
assert elbow_joint_parent_height < self.elbow_height
|
||||
h = (self.shoulder_height - shoulder_joint_child_height) / 2
|
||||
shoulder_h = self.shoulder_joint.child_height
|
||||
h = (self.shoulder_joint.height - shoulder_h) / 2
|
||||
tags_shoulder = [
|
||||
("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 = [
|
||||
("elbow_bot",
|
||||
self.elbow_to_abs(-elbow_mount_inset, h),
|
||||
self.elbow_angle + 90),
|
||||
("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),
|
||||
]
|
||||
profile = self.profile_s1()
|
||||
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:
|
||||
|
@ -513,33 +406,78 @@ class WingProfile:
|
|||
return profile
|
||||
def surface_s2(self,
|
||||
thickness: float = 25.4/16,
|
||||
elbow_mount_inset: float = 20,
|
||||
elbow_joint_child_height: float = 80,
|
||||
wrist_mount_inset: float = 20,
|
||||
wrist_joint_parent_height: float = 60,
|
||||
elbow_mount_inset: float = 0,
|
||||
wrist_mount_inset: float = 0,
|
||||
front: bool = True) -> Cq.Workplane:
|
||||
assert elbow_joint_child_height < self.elbow_height
|
||||
h = (self.elbow_height - elbow_joint_child_height) / 2
|
||||
elbow_h = self.elbow_joint.child_beam.total_height
|
||||
h = (self.elbow_height - elbow_h) / 2
|
||||
tags_elbow = [
|
||||
("elbow_bot",
|
||||
self.elbow_to_abs(elbow_mount_inset, h),
|
||||
self.elbow_angle + 90),
|
||||
("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),
|
||||
]
|
||||
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 = [
|
||||
("wrist_bot",
|
||||
self.wrist_to_abs(-wrist_mount_inset, h),
|
||||
self.wrist_angle + 90),
|
||||
("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),
|
||||
]
|
||||
profile = self.profile_s2()
|
||||
tags = tags_elbow + tags_wrist
|
||||
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:
|
||||
profile = (
|
||||
|
@ -549,61 +487,127 @@ class WingProfile:
|
|||
)
|
||||
return profile
|
||||
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:
|
||||
assert wrist_joint_child_height < self.wrist_height
|
||||
h = (self.wrist_height - wrist_joint_child_height) / 2
|
||||
wrist_mount_inset = 0
|
||||
wrist_h = self.wrist_joint.child_beam.total_height
|
||||
h = (self.wrist_height - wrist_h) / 2
|
||||
tags = [
|
||||
("wrist_bot",
|
||||
self.elbow_to_abs(wrist_mount_inset, h),
|
||||
self.elbow_angle + 90),
|
||||
self.wrist_to_abs(wrist_mount_inset, h),
|
||||
self.wrist_angle + 90),
|
||||
("wrist_top",
|
||||
self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height),
|
||||
self.elbow_angle - 90),
|
||||
self.wrist_to_abs(wrist_mount_inset, h + wrist_h),
|
||||
self.wrist_angle - 90),
|
||||
]
|
||||
profile = self.profile_s3()
|
||||
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
|
||||
|
||||
|
||||
def wing_r1s1_profile(self) -> Cq.Sketch:
|
||||
"""
|
||||
Generates the first wing segment profile, with the wing root pointing in
|
||||
the positive x axis.
|
||||
"""
|
||||
|
||||
|
||||
w = 270
|
||||
# Depression of the wing middle, measured
|
||||
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
|
||||
|
||||
return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
|
||||
def spacer_s3_wrist(self) -> MountingBox:
|
||||
holes = [
|
||||
Hole(x)
|
||||
for x in self.wrist_joint.child_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_s3(self) -> Cq.Assembly:
|
||||
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),
|
||||
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)
|
||||
)
|
||||
.spline([
|
||||
(0, 0),
|
||||
(w / 2, -h / 2 - bend),
|
||||
(w, -h),
|
||||
])
|
||||
.assemble()
|
||||
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
|
||||
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