cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
6 changed files with 201 additions and 283 deletions
Showing only changes of commit ef6b2a8663 - Show all commits

View File

@ -43,7 +43,7 @@ ROLE_COLOR_MAP = {
Role.PARENT: _color('blue4', 0.6),
Role.CASING: _color('dodgerblue3', 0.6),
Role.CHILD: _color('darkorange2', 0.6),
Role.DAMPING: _color('springgreen', 0.8),
Role.DAMPING: _color('springgreen', 1.0),
Role.STRUCTURE: _color('gray', 0.4),
Role.DECORATION: _color('lightseagreen', 0.4),
Role.ELECTRONIC: _color('mediumorchid', 0.5),

View File

@ -1,8 +1,8 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Optional
import math
import cadquery as Cq
import nhf.parts.springs as springs
from nhf.parts.springs import TorsionSpring
from nhf import Role
import nhf.utils
@ -158,99 +158,6 @@ class HirthJoint:
offset=offset)
return result.solve()
def comma_joint(radius=30,
shaft_radius=10,
height=10,
flange=10,
flange_thickness=25,
n_serration=16,
serration_angle_offset=0,
serration_height=5,
serration_inner_radius=20,
serration_theta=2 * math.pi / 48,
serration_tilt=-30,
right_handed=False):
"""
Produces a "o_" shaped joint, with serrations to accomodate a torsion spring
"""
assert flange_thickness <= radius
flange_poly = [
(0, radius - flange_thickness),
(0, radius),
(flange + radius, radius),
(flange + radius, radius - flange_thickness)
]
if right_handed:
flange_poly = [(x, -y) for x,y in flange_poly]
sketch = (
Cq.Sketch()
.circle(radius)
.polygon(flange_poly, mode='a')
.circle(shaft_radius, mode='s')
)
serration_poly = [
(0, 0), (radius, 0),
(radius, radius * math.tan(serration_theta))
]
serration = (
Cq.Workplane('XY')
.sketch()
.polygon(serration_poly)
.circle(radius, mode='i')
.circle(serration_inner_radius, mode='s')
.finalize()
.extrude(serration_height)
.translate(Cq.Vector((-serration_inner_radius, 0, height)))
.rotate(
axisStartPoint=(0, 0, 0),
axisEndPoint=(0, 0, height),
angleDegrees=serration_tilt)
.val()
)
serrations = (
Cq.Workplane('XY')
.polarArray(radius=serration_inner_radius,
startAngle=0+serration_angle_offset,
angle=360+serration_angle_offset,
count=n_serration)
.eachpoint(lambda loc: serration.located(loc))
)
result = (
Cq.Workplane()
.add(sketch)
.extrude(height)
.union(serrations)
.clean()
)
result.polyline([
(0, 0, height - serration_height),
(0, 0, height + serration_height)],
forConstruction=True).tag("serrated")
result.polyline([
(0, radius, 0),
(flange + radius, radius, 0)],
forConstruction=True).tag("tail")
result.faces('>X').tag("tail_end")
return result
def comma_assembly():
joint1 = comma_joint()
joint2 = comma_joint()
spring = springs.torsion_spring()
result = (
Cq.Assembly()
.add(joint1, name="joint1", color=Cq.Color(0.8,0.8,0.5,0.3))
.add(joint2, name="joint2", color=Cq.Color(0.8,0.8,0.5,0.3))
.add(spring, name="spring", color=Cq.Color(0.5,0.5,0.5,1))
.constrain("joint1?serrated", "spring?bot", "Plane")
.constrain("joint2?serrated", "spring?top", "Plane")
.constrain("joint1?tail", "FixedAxis", (1, 0, 0))
.constrain("joint2?tail", "FixedAxis", (-1, 0, 0))
.solve()
)
return result
@dataclass
class TorsionJoint:
"""
@ -268,6 +175,13 @@ class TorsionJoint:
2. A slotted annular extrusion where the slot allows the spring to rest
3. An outer and an inner annuli which forms a track the rider can move on
"""
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
radius=10.0,
thickness=2.0,
height=15.0,
tail_length=35.0,
right_handed=False,
))
# Radius limit for rotating components
radius_track: float = 40
@ -275,7 +189,6 @@ class TorsionJoint:
track_disk_height: float = 10
rider_disk_height: float = 8
radius_spring: float = 15
radius_axle: float = 6
# If true, cover the spring hole. May make it difficult to insert the spring
@ -283,12 +196,6 @@ class TorsionJoint:
spring_hole_cover_track: bool = False
spring_hole_cover_rider: bool = False
# Also used for the height of the hole for the spring
spring_thickness: float = 2
spring_height: float = 15
spring_tail_length: float = 35
groove_radius_outer: float = 35
groove_radius_inner: float = 20
# Gap on inner groove to ease movement
@ -301,23 +208,19 @@ class TorsionJoint:
rider_slot_begin: float = 0
rider_slot_span: float = 90
right_handed: bool = False
def __post_init__(self):
assert self.radius_track > self.groove_radius_outer
assert self.radius_rider > self.groove_radius_outer
assert self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap
assert self.groove_radius_inner > self.radius_spring
assert self.spring_height > self.groove_depth, "Groove is too deep"
assert self.radius_spring > self.radius_axle
assert self.radius_rider > self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap
assert self.groove_radius_inner > self.spring.radius > self.radius_axle
assert self.spring.height > self.groove_depth, "Groove is too deep"
@property
def total_height(self):
"""
Total height counting from bottom to top
"""
return self.track_disk_height + self.rider_disk_height + self.spring_height
return self.track_disk_height + self.rider_disk_height + self.spring.height
@property
def radius(self):
@ -326,28 +229,24 @@ class TorsionJoint:
"""
return max(self.radius_rider, self.radius_track)
@property
def _radius_spring_internal(self):
return self.radius_spring - self.spring_thickness
def _slot_polygon(self, flip: bool=False):
r1 = self.radius_spring - self.spring_thickness
r2 = self.radius_spring
flip = flip != self.right_handed
r1 = self.spring.radius_inner
r2 = self.spring.radius
flip = flip != self.spring.right_handed
if flip:
r1 = -r1
r2 = -r2
return [
(0, r2),
(self.spring_tail_length, r2),
(self.spring_tail_length, r1),
(self.spring.tail_length, r2),
(self.spring.tail_length, r1),
(0, r1),
]
def _directrix(self, height, theta=0):
c, s = math.cos(theta), math.sin(theta)
r2 = self.radius_spring
l = self.spring_tail_length
if self.right_handed:
r2 = self.spring.radius
l = self.spring.tail_length
if self.spring.right_handed:
r2 = -r2
# This is (0, r2) and (l, r2) transformed by right handed rotation
# matrix `[[c, -s], [s, c]]`
@ -356,16 +255,6 @@ class TorsionJoint:
(c * l - s * r2, s * l + c * r2, height),
]
def spring(self):
return springs.torsion_spring(
radius=self.radius_spring,
height=self.spring_height,
thickness=self.spring_thickness,
tail_length=self.spring_tail_length,
right_handed=self.right_handed,
)
def track(self):
# TODO: Cover outer part of track only. Can we do this?
groove_profile = (
@ -373,14 +262,14 @@ class TorsionJoint:
.circle(self.radius_track)
.circle(self.groove_radius_outer, mode='s')
.circle(self.groove_radius_inner, mode='a')
.circle(self.radius_spring, mode='s')
.circle(self.spring.radius, mode='s')
)
spring_hole_profile = (
Cq.Sketch()
.circle(self.radius_track)
.circle(self.radius_spring, mode='s')
.circle(self.spring.radius, mode='s')
)
slot_height = self.spring_thickness
slot_height = self.spring.thickness
if not self.spring_hole_cover_track:
slot_height += self.groove_depth
slot = (
@ -400,7 +289,7 @@ class TorsionJoint:
.faces('>Z')
.tag("spring")
.placeSketch(spring_hole_profile)
.extrude(self.spring_thickness)
.extrude(self.spring.thickness)
# If the spring hole profile is not simply connected, this workplane
# will have to be created from the `spring-mate` face.
.faces('>Z')
@ -425,7 +314,7 @@ class TorsionJoint:
wall_profile = (
Cq.Sketch()
.circle(self.radius_rider, mode='a')
.circle(self.radius_spring, mode='s')
.circle(self.spring.radius, mode='s')
.parray(
r=0,
a1=rider_slot_begin,
@ -451,7 +340,7 @@ class TorsionJoint:
.reset()
)
#.circle(self._radius_wall, mode='a')
middle_height = self.spring_height - self.groove_depth - self.rider_gap - self.spring_thickness
middle_height = self.spring.height - self.groove_depth - self.rider_gap - self.spring.thickness
result = (
Cq.Workplane('XY')
.cylinder(
@ -471,8 +360,8 @@ class TorsionJoint:
.extrude(self.groove_depth + self.rider_gap)
.faces(tag="spring")
.workplane()
.circle(self._radius_spring_internal)
.extrude(self.spring_height)
.circle(self.spring.radius_inner)
.extrude(self.spring.height)
.faces("<Z")
.workplane()
.hole(self.radius_axle * 2)
@ -490,10 +379,10 @@ class TorsionJoint:
forConstruction=True).tag(f"dir{j}")
return result
def rider_track_assembly(self, directrix=0):
def rider_track_assembly(self, directrix: int = 0, deflection: float = 0):
rider = self.rider()
track = self.track()
spring = self.spring()
spring = self.spring.generate(deflection=deflection)
result = (
Cq.Assembly()
.addS(spring, name="spring", role=Role.DAMPING)

View File

@ -1,59 +1,75 @@
import math
from typing import Optional
from dataclasses import dataclass
import cadquery as Cq
def torsion_spring(radius=12,
height=20,
thickness=2,
omega=90,
tail_length=25,
right_handed: bool = False):
@dataclass
class TorsionSpring:
"""
Produces a torsion spring with abridged geometry since sweep is very slow in
cq-editor.
A torsion spring with abridged geometry (since sweep is slow)
"""
if right_handed:
omega = -omega
# Outer radius
radius: float = 12.0
height: float = 20.0
thickness: float = 2.0
# Angle (in degrees) between the two legs at neutral position
angle_neutral: float = 90.0
tail_length: float = 25.0
right_handed: bool = False
torsion_rate: Optional[float] = None
@property
def radius_inner(self) -> float:
return self.radius - self.thickness
def torque_at(self, theta: float) -> float:
return self.torsion_rate * theta
def generate(self, deflection: float = 0):
omega = self.angle_neutral + deflection
omega = -omega if self.right_handed else omega
base = (
Cq.Workplane('XY')
.cylinder(height=height, radius=radius,
.cylinder(height=self.height, radius=self.radius,
centered=(True, True, False))
)
base.faces(">Z").tag("top")
base.faces("<Z").tag("bot")
box_shift = -radius if right_handed else radius-thickness
box_shift = -self.radius if self.right_handed else self.radius-self.thickness
tail = Cq.Solid.makeCylinder(
height=self.tail_length,
radius=self.thickness / 2)
# points cylinder to +X
dy = self.radius - self.thickness / 2
if self.right_handed:
dy = -dy
loc_dir_x = Cq.Location((0, 0, self.thickness / 2), (0, 1, 0), 90)
loc_shift = Cq.Location((0, dy, 0))
loc_top = Cq.Location((0, 0, self.height - self.thickness), (0, 0, 1), omega + 180)
result = (
base
.cylinder(height=height, radius=radius - thickness, combine='s',
.cylinder(
height=self.height,
radius=self.radius - self.thickness,
combine='s',
centered=(True, True, True))
.transformed(
offset=(0, box_shift),
rotate=(0, 0, 0))
.box(
length=tail_length,
width=thickness,
height=thickness,
centered=False)
.copyWorkplane(Cq.Workplane('XY'))
.transformed(
offset=(0, 0, height - thickness),
rotate=(0, 0, omega))
.center(-tail_length, box_shift)
.box(
length=tail_length,
width=thickness,
height=thickness,
centered=False)
.union(tail.located(loc_shift * loc_dir_x))
.union(tail.located(loc_top * loc_shift.inverse * loc_dir_x))
.clean()
)
r = -radius if right_handed else radius
r = -self.radius if self.right_handed else self.radius
plane = result.copyWorkplane(Cq.Workplane('XY'))
plane.polyline([(0, r, 0), (tail_length, r, 0)],
plane.polyline([(0, r, 0), (self.tail_length, r, 0)],
forConstruction=True).tag("dir_bot")
omega = math.radians(omega)
c, s = math.cos(omega), math.sin(omega)
l = -tail_length
l = -self.tail_length
plane.polyline([
(-s * r, c * r, height),
(c * l - s * r, c * r + s * l, height)],
(-s * r, c * r, self.height),
(c * l - s * r, c * r + s * l, self.height)],
forConstruction=True).tag("dir_top")
return result

View File

@ -20,8 +20,6 @@ class TestJoints(unittest.TestCase):
isect = binary_intersection(assembly)
self.assertLess(isect.Volume(), 1e-6,
"Hirth joint assembly must not have intersection")
def test_joints_comma_assembly(self):
joints.comma_assembly()
def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int):
assert 0 <= slot and slot < joint.rider_n_slots

View File

@ -1,9 +1,10 @@
import math
from dataclasses import dataclass, field
from typing import Optional, Tuple
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
import nhf.parts.springs as springs
from nhf.parts.springs import TorsionSpring
from nhf.parts.joints import TorsionJoint
from nhf.parts.box import box_with_centre_holes
import nhf.utils
@ -23,10 +24,12 @@ class ShoulderJoint(Model):
rider_disk_height=5.0,
# M8 Axle
radius_axle=3.0,
spring=TorsionSpring(
# inner diameter = 9
radius_spring=9/2 + 1.2,
spring_thickness=1.3,
spring_height=7.5,
radius=9/2 + 1.2,
thickness=1.3,
height=7.5,
),
))
# On the parent side, drill vertical holes
@ -189,12 +192,12 @@ class ShoulderJoint(Model):
.addS(self.child(), name="child",
role=Role.CHILD, material=mat)
.constrain("child/core", "Fixed")
.addS(self.torsion_joint.spring(), name="spring_top",
.addS(self.torsion_joint.spring.generate(), name="spring_top",
role=Role.DAMPING, material=mat_spring)
.addS(self.parent(wing_root_wall_thickness),
name="parent_top",
role=Role.PARENT, material=mat)
.addS(self.torsion_joint.spring(), name="spring_bot",
.addS(self.torsion_joint.spring.generate(), name="spring_bot",
role=Role.DAMPING, material=mat_spring)
.addS(self.parent(wing_root_wall_thickness),
name="parent_bot",
@ -280,25 +283,27 @@ class DiskJoint(Model):
"""
Sandwiched disk joint for the wrist and elbow
"""
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
radius=9 / 2,
thickness=1.3,
height=6.5,
tail_length=45.0,
right_handed=False,
))
radius_housing: float = 22.0
radius_disk: float = 20.0
radius_spring: float = 9 / 2
radius_axle: float = 3.0
housing_thickness: float = 5.0
disk_thickness: float = 5.0
# Gap between disk and the housing
#disk_thickness_gap: float = 0.1
spring_thickness: float = 1.3
spring_height: float = 6.5
spring_tail_length: float = 45.0
# Spring angle at 0 degrees of movement
spring_angle: float = 30.0
# Angle at which the spring exerts no torque
spring_angle_neutral: float = 90.0
spring_angle_shift: float = 30
spring_angle_at_0: float = 30.0
spring_slot_offset: float = 15.0
wall_inset: float = 2.0
# Angular span of movement
@ -317,18 +322,9 @@ class DiskJoint(Model):
assert self.radius_disk > self.radius_axle
assert self.housing_upper_carve_offset > 0
def spring(self):
return springs.torsion_spring(
radius=self.radius_spring,
height=self.spring_height,
thickness=self.spring_thickness,
tail_length=self.spring_tail_length,
right_handed=False,
)
@property
def neutral_movement_angle(self) -> Optional[float]:
a = self.spring_angle_neutral - self.spring_angle
a = self.spring.angle_neutral - self.spring_angle_at_0
if 0 <= a and a <= self.movement_angle:
return a
return None
@ -346,7 +342,7 @@ class DiskJoint(Model):
"""
Distance between the spring track and the outside of the upper housing
"""
return self.housing_thickness + self.disk_thickness - self.spring_height
return self.housing_thickness + self.disk_thickness - self.spring.height
@property
def housing_upper_dz(self) -> float:
@ -355,42 +351,17 @@ class DiskJoint(Model):
"""
return self.total_thickness / 2 - self.housing_thickness
@property
def radius_spring_internal(self):
return self.radius_spring - self.spring_thickness
@target(name="disk")
def disk(self) -> Cq.Workplane:
cut = (
Cq.Solid.makeBox(
length=self.spring_tail_length,
width=self.spring_thickness,
length=self.spring.tail_length,
width=self.spring.thickness,
height=self.disk_thickness,
)
.located(Cq.Location((0, self.radius_spring_internal, 0)))
.rotate((0, 0, 0), (0, 0, 1), self.spring_angle_shift)
.located(Cq.Location((0, self.spring.radius_inner, 0)))
.rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset)
)
result = (
Cq.Workplane('XY')
.cylinder(
height=self.disk_thickness,
radius=self.radius_disk,
centered=(True, True, False)
)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.disk_thickness,
radius=self.radius_spring,
centered=(True, True, False),
combine='cut',
)
.cut(cut)
)
plane = result.copyWorkplane(Cq.Workplane('XY'))
plane.tagPlane("dir", direction="+X")
plane.workplane(offset=self.disk_thickness).tagPlane("mate_top")
result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot")
radius_tongue = self.radius_disk + self.tongue_length
tongue = (
Cq.Solid.makeCylinder(
@ -402,7 +373,29 @@ class DiskJoint(Model):
radius=self.radius_disk,
))
)
result = result.union(tongue, tol=TOL)
result = (
Cq.Workplane('XY')
.cylinder(
height=self.disk_thickness,
radius=self.radius_disk,
centered=(True, True, False)
)
.union(tongue, tol=TOL)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.disk_thickness,
radius=self.spring.radius,
centered=(True, True, False),
combine='cut',
)
.cut(cut)
)
plane = result.copyWorkplane(Cq.Workplane('XY'))
theta = math.radians(self.spring_slot_offset)
plane.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0))
plane.workplane(offset=self.disk_thickness).tagPlane("mate_top")
result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot")
return result
def wall(self) -> Cq.Compound:
@ -433,9 +426,6 @@ class DiskJoint(Model):
)
result.faces(">Z").tag("mate")
result.faces(">Z").workplane().tagPlane("dirX", direction="+X")
# two directional vectors are required to make the angle constrain
# unambiguous
result.faces(">Z").workplane().tagPlane("dirY", direction="+Y")
result = result.cut(
self
.wall()
@ -447,16 +437,17 @@ class DiskJoint(Model):
@target(name="housing-upper")
def housing_upper(self) -> Cq.Workplane:
carve_angle = -(self.spring_angle_at_0 - self.spring_slot_offset)
carve = (
Cq.Solid.makeCylinder(
radius=self.radius_spring,
radius=self.spring.radius,
height=self.housing_thickness
).fuse(Cq.Solid.makeBox(
length=self.spring_tail_length,
width=self.spring_thickness,
length=self.spring.tail_length,
width=self.spring.thickness,
height=self.housing_thickness
).located(Cq.Location((0, -self.radius_spring, 0))))
).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift)
).located(Cq.Location((0, -self.spring.radius, 0))))
).rotate((0, 0, 0), (0, 0, 1), carve_angle)
result = (
Cq.Workplane('XY')
.cylinder(
@ -465,8 +456,11 @@ class DiskJoint(Model):
centered=(True, True, False),
)
)
theta = math.radians(carve_angle)
result.faces("<Z").tag("mate")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X")
p_xy = result.copyWorkplane(Cq.Workplane('XY'))
p_xy.tagPlane("dirX", direction="+X")
p_xy.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0))
result = result.faces(">Z").hole(self.radius_axle * 2)
# tube which holds the spring interior
@ -490,28 +484,42 @@ class DiskJoint(Model):
.cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset))))
.union(wall, tol=TOL)
)
return result
return result.clean()
def add_constraints(self,
assembly: Cq.Assembly,
housing_lower: str,
housing_upper: str,
disk: str,
angle: float,
angle: float = 0.0,
) -> Cq.Assembly:
return (
deflection = angle - self.neutral_movement_angle
spring_name = disk.replace("/", "__Z") + "_spring"
(
assembly
.addS(
self.spring.generate(deflection=-deflection),
name=spring_name,
role=Role.DAMPING,
material=Material.STEEL_SPRING)
.constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane")
.constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane")
.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0)
.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle)
.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90)
.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dirX", "Axis", param=0)
.constrain(f"{housing_upper}?dir", f"{spring_name}?dir_top", "Axis", param=0)
.constrain(f"{spring_name}?dir_bot", f"{disk}?dir", "Axis", param=0)
.constrain(f"{disk}?mate_bot", f"{spring_name}?bot", "Plane", param=0)
#.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0)
#.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle)
#.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90)
)
return (
assembly
)
def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly:
if angle is None:
angle = self.movement_angle
angle = self.neutral_movement_angle
if angle is None:
angle = 0
else:
@ -521,7 +529,7 @@ class DiskJoint(Model):
.addS(self.disk(), name="disk", role=Role.CHILD)
.addS(self.housing_lower(), name="housing_lower", role=Role.PARENT)
.addS(self.housing_upper(), name="housing_upper", role=Role.CASING)
#.constrain("housing_lower", "Fixed")
.constrain("housing_lower", "Fixed")
)
result = self.add_constraints(
result,
@ -627,10 +635,15 @@ class ElbowJoint:
.rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2)
)
housing = self.disk_joint.housing_upper()
housing_loc = Cq.Location(
(0, 0, housing_dz),
(0, 0, 1),
-self.disk_joint.tongue_span / 2
)
result = (
self.parent_beam.beam()
.add(housing, name="housing",
loc=axial_offset * Cq.Location((0, 0, housing_dz)))
loc=axial_offset * housing_loc)
.add(connector, name="connector",
loc=axial_offset)
#.constrain("housing", "Fixed")

View File

@ -545,7 +545,8 @@ class WingProfile(Model):
return result.solve()
def assembly(self,
parts: Optional[list[str]] = None
parts: Optional[list[str]] = None,
angle_elbow_wrist: float = 0.0,
) -> Cq.Assembly():
if parts is None:
parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
@ -575,7 +576,7 @@ class WingProfile(Model):
.constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane")
)
if "elbow" in parts:
result.add(self.elbow_joint.assembly(), name="elbow")
result.add(self.elbow_joint.assembly(angle=angle_elbow_wrist), name="elbow")
if "s1" in parts and "elbow" in parts:
(
result
@ -595,24 +596,25 @@ class WingProfile(Model):
.constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane")
)
if "wrist" in parts:
result.add(self.wrist_joint.assembly(), name="wrist")
result.add(self.wrist_joint.assembly(angle=angle_elbow_wrist), name="wrist")
if "s2" in parts and "wrist" in parts:
# Mounted backwards to bend in other direction
(
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")
.constrain("s2/wrist_top?conn0", "wrist/parent_upper/bot?conn0", "Plane")
.constrain("s2/wrist_top?conn1", "wrist/parent_upper/bot?conn1", "Plane")
.constrain("s2/wrist_bot?conn0", "wrist/parent_upper/top?conn0", "Plane")
.constrain("s2/wrist_bot?conn1", "wrist/parent_upper/top?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")
.constrain("s3/wrist_top?conn0", "wrist/child/bot?conn0", "Plane")
.constrain("s3/wrist_top?conn1", "wrist/child/bot?conn1", "Plane")
.constrain("s3/wrist_bot?conn0", "wrist/child/top?conn0", "Plane")
.constrain("s3/wrist_bot?conn1", "wrist/child/top?conn1", "Plane")
)
if len(parts) > 1:
result.solve()