cosplay: Touhou/Houjuu Nue #4
|
@ -1,6 +1,7 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import math
|
import math
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
|
from typing import Optional
|
||||||
from nhf import Item, Role
|
from nhf import Item, Role
|
||||||
import nhf.utils
|
import nhf.utils
|
||||||
|
|
||||||
|
@ -127,3 +128,35 @@ class HexNut(Item):
|
||||||
.regularPolygon(r=self.radius, n=6)
|
.regularPolygon(r=self.radius, n=6)
|
||||||
._faces
|
._faces
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Washer(Item):
|
||||||
|
diam_thread: float
|
||||||
|
diam_outer: float
|
||||||
|
thickness: float
|
||||||
|
material_name: Optional[float] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.diam_outer > self.diam_thread
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
suffix = (" " + self.material_name) if self.material_name else ""
|
||||||
|
return f"Washer M{int(self.diam_thread)}{suffix}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def role(self) -> Role:
|
||||||
|
return Role.CONNECTION
|
||||||
|
|
||||||
|
def generate(self) -> Cq.Workplane:
|
||||||
|
result = (
|
||||||
|
Cq.Workplane('XY')
|
||||||
|
.cylinder(
|
||||||
|
radius=self.diam_outer/2,
|
||||||
|
height=self.thickness,
|
||||||
|
)
|
||||||
|
.faces(">Z")
|
||||||
|
.hole(self.diam_thread)
|
||||||
|
)
|
||||||
|
result.faces("<Z").tag("bot")
|
||||||
|
result.faces(">Z").tag("top")
|
||||||
|
return result
|
||||||
|
|
|
@ -547,3 +547,9 @@ class ElectronicBoard(Model):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return result.solve()
|
return result.solve()
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LightStrip:
|
||||||
|
|
||||||
|
width: float = 10.0
|
||||||
|
height: float = 4.5
|
||||||
|
|
|
@ -6,7 +6,7 @@ from nhf import Material, Role
|
||||||
from nhf.build import Model, target, assembly
|
from nhf.build import Model, target, assembly
|
||||||
from nhf.parts.box import MountingBox
|
from nhf.parts.box import MountingBox
|
||||||
from nhf.parts.springs import TorsionSpring
|
from nhf.parts.springs import TorsionSpring
|
||||||
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
|
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob, Washer
|
||||||
from nhf.parts.joints import TorsionJoint, HirthJoint
|
from nhf.parts.joints import TorsionJoint, HirthJoint
|
||||||
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
|
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
|
||||||
from nhf.touhou.houjuu_nue.electronics import (
|
from nhf.touhou.houjuu_nue.electronics import (
|
||||||
|
@ -67,6 +67,28 @@ ELBOW_TORSION_SPRING = TorsionSpring(
|
||||||
right_handed=False,
|
right_handed=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ELBOW_AXLE_BOLT = FlatHeadBolt(
|
||||||
|
mass=0.0,
|
||||||
|
diam_head=6.87,
|
||||||
|
height_head=3.06,
|
||||||
|
diam_thread=4.0,
|
||||||
|
height_thread=20.0,
|
||||||
|
)
|
||||||
|
ELBOW_AXLE_WASHER = Washer(
|
||||||
|
mass=0.0,
|
||||||
|
diam_outer=8.96,
|
||||||
|
diam_thread=4.0,
|
||||||
|
thickness=1.02,
|
||||||
|
material_name="Nylon"
|
||||||
|
)
|
||||||
|
ELBOW_AXLE_HEX_NUT = HexNut(
|
||||||
|
mass=0.0,
|
||||||
|
diam_thread=4.0,
|
||||||
|
pitch=0.7,
|
||||||
|
thickness=3.6, # or 2.64 for metal
|
||||||
|
width=6.89,
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RootJoint(Model):
|
class RootJoint(Model):
|
||||||
"""
|
"""
|
||||||
|
@ -390,12 +412,13 @@ class ShoulderJoint(Model):
|
||||||
angle_neutral: float = -15.0
|
angle_neutral: float = -15.0
|
||||||
angle_max_deflection: float = 65.0
|
angle_max_deflection: float = 65.0
|
||||||
|
|
||||||
spool_radius: float = 12.0
|
spool_radius_diff: float = 2.0
|
||||||
spool_groove_depth: float = 1.0
|
# All the heights here are mirrored for the bottom as well
|
||||||
spool_base_height: float = 3.0
|
spool_cap_height: float = 3.0
|
||||||
spool_height: float = 5.0
|
spool_core_height: float = 2.0
|
||||||
spool_cap_height: float = 1.0
|
|
||||||
spool_groove_inset: float = 3.0
|
spool_line_thickness: float = 1.2
|
||||||
|
spool_groove_radius: float = 10.0
|
||||||
|
|
||||||
flip: bool = False
|
flip: bool = False
|
||||||
actuator: LinearActuator = LINEAR_ACTUATOR_21
|
actuator: LinearActuator = LINEAR_ACTUATOR_21
|
||||||
|
@ -403,11 +426,17 @@ class ShoulderJoint(Model):
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
assert self.parent_lip_length * 2 < self.height
|
assert self.parent_lip_length * 2 < self.height
|
||||||
assert self.child_guard_ext > self.torsion_joint.radius_rider
|
assert self.child_guard_ext > self.torsion_joint.radius_rider
|
||||||
assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness
|
assert self.spool_groove_radius < self.spool_inner_radius < self.spool_outer_radius
|
||||||
assert self.spool_base_height > self.spool_groove_depth
|
|
||||||
assert self.child_lip_height < self.height
|
assert self.child_lip_height < self.height
|
||||||
assert self.draft_length <= self.actuator.stroke_length
|
assert self.draft_length <= self.actuator.stroke_length
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spool_outer_radius(self):
|
||||||
|
return self.torsion_joint.radius_rider - self.child_core_thickness
|
||||||
|
@property
|
||||||
|
def spool_inner_radius(self):
|
||||||
|
return self.spool_outer_radius - self.spool_radius_diff
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def radius(self):
|
def radius(self):
|
||||||
return self.torsion_joint.radius
|
return self.torsion_joint.radius
|
||||||
|
@ -417,7 +446,7 @@ class ShoulderJoint(Model):
|
||||||
"""
|
"""
|
||||||
Amount of wires that need to draft on the spool
|
Amount of wires that need to draft on the spool
|
||||||
"""
|
"""
|
||||||
return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection)
|
return self.spool_inner_radius * math.radians(self.angle_max_deflection)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def draft_height(self):
|
def draft_height(self):
|
||||||
|
@ -444,16 +473,20 @@ class ShoulderJoint(Model):
|
||||||
def _max_contraction_angle(self) -> float:
|
def _max_contraction_angle(self) -> float:
|
||||||
return 180 - self.angle_max_deflection + self.angle_neutral
|
return 180 - self.angle_max_deflection + self.angle_neutral
|
||||||
|
|
||||||
def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid:
|
def _contraction_cut_angle(self) -> float:
|
||||||
"""
|
|
||||||
Generates a cylindrical sector which cuts away overlapping regions of the child and parent
|
|
||||||
"""
|
|
||||||
aspect = self.child_guard_width / self.parent_arm_width
|
aspect = self.child_guard_width / self.parent_arm_width
|
||||||
theta = math.radians(self._max_contraction_angle)
|
theta = math.radians(self._max_contraction_angle)
|
||||||
#theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect))
|
#theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect))
|
||||||
theta_p = math.atan2(math.sin(theta), math.cos(theta) + aspect)
|
theta_p = math.atan2(math.sin(theta), math.cos(theta) + aspect)
|
||||||
angle = math.degrees(theta_p)
|
angle = math.degrees(theta_p)
|
||||||
assert 0 <= angle <= 90
|
assert 0 <= angle <= 90
|
||||||
|
return angle
|
||||||
|
|
||||||
|
def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid:
|
||||||
|
"""
|
||||||
|
Generates a cylindrical sector which cuts away overlapping regions of the child and parent
|
||||||
|
"""
|
||||||
|
angle = self._contraction_cut_angle()
|
||||||
# outer radius of the cut, overestimated
|
# outer radius of the cut, overestimated
|
||||||
cut_radius = math.sqrt(self.child_guard_width ** 2 + self.parent_arm_width ** 2)
|
cut_radius = math.sqrt(self.child_guard_width ** 2 + self.parent_arm_width ** 2)
|
||||||
span = 180
|
span = 180
|
||||||
|
@ -550,37 +583,39 @@ class ShoulderJoint(Model):
|
||||||
joint = self.torsion_joint
|
joint = self.torsion_joint
|
||||||
return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height
|
return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height
|
||||||
|
|
||||||
def _spool(self) -> Cq.Workplane:
|
def _spool(self) -> Cq.Compound:
|
||||||
"""
|
"""
|
||||||
Generates the spool piece which holds the line in tension
|
Generates the spool piece which holds the line in tension
|
||||||
"""
|
"""
|
||||||
t = self.spool_groove_depth
|
t = self.spool_line_thickness
|
||||||
radius_core_inner = self.torsion_joint.radius_rider - self.child_core_thickness
|
|
||||||
spindle = Cq.Solid.makeCone(
|
spindle = Cq.Solid.makeCone(
|
||||||
radius1=self.spool_radius,
|
radius1=self.spool_inner_radius,
|
||||||
radius2=radius_core_inner,
|
radius2=self.spool_outer_radius,
|
||||||
height=self.spool_height,
|
height=self.spool_core_height,
|
||||||
)
|
)
|
||||||
cap = Cq.Solid.makeCylinder(
|
cap = Cq.Solid.makeCylinder(
|
||||||
radius=radius_core_inner,
|
radius=self.spool_outer_radius,
|
||||||
height=self.spool_cap_height
|
height=self.spool_cap_height
|
||||||
).located(Cq.Location((0,0,self.spool_height)))
|
).moved(Cq.Location((0,0,self.spool_core_height)))
|
||||||
hole_x = radius_core_inner - self.spool_groove_inset
|
cut_height = self.spool_cap_height + self.spool_core_height
|
||||||
slot = Cq.Solid.makeBox(
|
cut_hole = Cq.Solid.makeCylinder(
|
||||||
length=t,
|
radius=t / 2,
|
||||||
|
height=cut_height,
|
||||||
|
).moved(Cq.Location((self.spool_groove_radius, 0, 0)))
|
||||||
|
cut_slot = Cq.Solid.makeBox(
|
||||||
|
length=self.spool_outer_radius - self.spool_groove_radius,
|
||||||
width=t,
|
width=t,
|
||||||
height=self.spool_base_height,
|
height=self.spool_core_height,
|
||||||
).located(Cq.Location((hole_x, -t/2, 0)))
|
).moved(Cq.Location((self.spool_groove_radius, -t/2, 0)))
|
||||||
hole = Cq.Solid.makeBox(
|
cut_centre_hole = Cq.Solid.makeCylinder(
|
||||||
length=t,
|
|
||||||
width=t,
|
|
||||||
height=self.spool_height + self.spool_base_height,
|
|
||||||
).located(Cq.Location((hole_x, -t/2, 0)))
|
|
||||||
centre_hole = Cq.Solid.makeCylinder(
|
|
||||||
radius=self.torsion_joint.radius_axle,
|
radius=self.torsion_joint.radius_axle,
|
||||||
height=self.spool_height + self.spool_base_height,
|
height=cut_height,
|
||||||
|
)
|
||||||
|
top = spindle.fuse(cap).cut(cut_hole, cut_centre_hole, cut_slot)
|
||||||
|
return (
|
||||||
|
top
|
||||||
|
.fuse(top.located(Cq.Location((0,0,0), (1,0, 0), 180)))
|
||||||
)
|
)
|
||||||
return spindle.fuse(cap).cut(slot, hole, centre_hole)
|
|
||||||
|
|
||||||
@target(name="child")
|
@target(name="child")
|
||||||
def child(self) -> Cq.Assembly:
|
def child(self) -> Cq.Assembly:
|
||||||
|
@ -611,6 +646,14 @@ class ShoulderJoint(Model):
|
||||||
.assemble()
|
.assemble()
|
||||||
.circle(radius_core_inner, mode='s')
|
.circle(radius_core_inner, mode='s')
|
||||||
)
|
)
|
||||||
|
angle_line_span = -self.angle_neutral + self.angle_max_deflection + 90
|
||||||
|
angle_line = 180 - angle_line_span
|
||||||
|
# leave space for the line to rotate
|
||||||
|
spool_cut = Cq.Solid.makeCylinder(
|
||||||
|
radius=joint.radius_rider * 2,
|
||||||
|
height=self.spool_core_height * 2,
|
||||||
|
angleDegrees=angle_line_span,
|
||||||
|
).moved(Cq.Location((0,0,-self.spool_core_height), (0,0,1), angle_line))
|
||||||
lip_extension = (
|
lip_extension = (
|
||||||
Cq.Solid.makeBox(
|
Cq.Solid.makeBox(
|
||||||
length=self.child_lip_ext - self.child_guard_ext,
|
length=self.child_lip_ext - self.child_guard_ext,
|
||||||
|
@ -676,7 +719,8 @@ class ShoulderJoint(Model):
|
||||||
.toPending()
|
.toPending()
|
||||||
.extrude(dh * 2)
|
.extrude(dh * 2)
|
||||||
.translate(Cq.Vector(0, 0, -dh))
|
.translate(Cq.Vector(0, 0, -dh))
|
||||||
.union(core_guard)
|
.cut(spool_cut)
|
||||||
|
.union(core_guard, tol=TOL)
|
||||||
)
|
)
|
||||||
assert self.child_lip_width / 2 <= joint.radius_rider
|
assert self.child_lip_width / 2 <= joint.radius_rider
|
||||||
sign = 1 if self.flip else -1
|
sign = 1 if self.flip else -1
|
||||||
|
@ -695,8 +739,8 @@ class ShoulderJoint(Model):
|
||||||
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
|
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
|
||||||
loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot + self.angle_neutral)
|
loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot + self.angle_neutral)
|
||||||
loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral)
|
loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral)
|
||||||
spool_dz = self.height / 2 - self.torsion_joint.total_height
|
spool_dz = 0
|
||||||
spool_angle = 180 + self.angle_neutral
|
spool_angle = -self.angle_neutral
|
||||||
loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location()
|
loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location()
|
||||||
result = (
|
result = (
|
||||||
Cq.Assembly()
|
Cq.Assembly()
|
||||||
|
@ -848,6 +892,7 @@ class DiskJoint(Model):
|
||||||
|
|
||||||
housing_thickness: float = 4.0
|
housing_thickness: float = 4.0
|
||||||
disk_thickness: float = 8.0
|
disk_thickness: float = 8.0
|
||||||
|
tongue_thickness: float = 10.0
|
||||||
|
|
||||||
# Amount by which the wall carves in
|
# Amount by which the wall carves in
|
||||||
wall_inset: float = 2.0
|
wall_inset: float = 2.0
|
||||||
|
@ -868,6 +913,9 @@ class DiskJoint(Model):
|
||||||
|
|
||||||
generate_inner_wall: bool = False
|
generate_inner_wall: bool = False
|
||||||
|
|
||||||
|
axle_bolt: FlatHeadBolt = ELBOW_AXLE_BOLT
|
||||||
|
axle_washer: Washer = ELBOW_AXLE_WASHER
|
||||||
|
axle_hex_nut: HexNut = ELBOW_AXLE_HEX_NUT
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
super().__init__(name="disk-joint")
|
super().__init__(name="disk-joint")
|
||||||
|
@ -876,6 +924,11 @@ class DiskJoint(Model):
|
||||||
|
|
||||||
assert self.housing_upper_carve_offset > 0
|
assert self.housing_upper_carve_offset > 0
|
||||||
assert self.spring_tail_hole_height > self.spring.thickness
|
assert self.spring_tail_hole_height > self.spring.thickness
|
||||||
|
assert self.tongue_thickness <= self.total_thickness
|
||||||
|
|
||||||
|
assert self.axle_bolt.diam_thread == self.axle_washer.diam_thread
|
||||||
|
assert self.axle_bolt.diam_thread == self.axle_hex_nut.diam_thread
|
||||||
|
assert self.axle_bolt.height_thread > self.total_thickness, "Bolt is not long enough"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def neutral_movement_angle(self) -> Optional[float]:
|
def neutral_movement_angle(self) -> Optional[float]:
|
||||||
|
@ -932,10 +985,21 @@ class DiskJoint(Model):
|
||||||
@target(name="disk")
|
@target(name="disk")
|
||||||
def disk(self) -> Cq.Workplane:
|
def disk(self) -> Cq.Workplane:
|
||||||
radius_tongue = self.radius_disk + self.tongue_length
|
radius_tongue = self.radius_disk + self.tongue_length
|
||||||
tongue = (
|
outer_tongue = (
|
||||||
|
Cq.Solid.makeCylinder(
|
||||||
|
height=self.tongue_thickness,
|
||||||
|
radius=radius_tongue,
|
||||||
|
angleDegrees=self.tongue_span,
|
||||||
|
).cut(Cq.Solid.makeCylinder(
|
||||||
|
height=self.tongue_thickness,
|
||||||
|
radius=self.radius_housing,
|
||||||
|
))
|
||||||
|
.moved(Cq.Location((0,0,(self.disk_thickness - self.tongue_thickness) / 2)))
|
||||||
|
)
|
||||||
|
inner_tongue = (
|
||||||
Cq.Solid.makeCylinder(
|
Cq.Solid.makeCylinder(
|
||||||
height=self.disk_thickness,
|
height=self.disk_thickness,
|
||||||
radius=radius_tongue,
|
radius=self.radius_housing,
|
||||||
angleDegrees=self.tongue_span,
|
angleDegrees=self.tongue_span,
|
||||||
).cut(Cq.Solid.makeCylinder(
|
).cut(Cq.Solid.makeCylinder(
|
||||||
height=self.disk_thickness,
|
height=self.disk_thickness,
|
||||||
|
@ -949,7 +1013,8 @@ class DiskJoint(Model):
|
||||||
radius=self.radius_disk,
|
radius=self.radius_disk,
|
||||||
centered=(True, True, False)
|
centered=(True, True, False)
|
||||||
)
|
)
|
||||||
.union(tongue, tol=TOL)
|
.union(inner_tongue, tol=TOL)
|
||||||
|
.union(outer_tongue, tol=TOL)
|
||||||
.copyWorkplane(Cq.Workplane('XY'))
|
.copyWorkplane(Cq.Workplane('XY'))
|
||||||
.cylinder(
|
.cylinder(
|
||||||
height=self.disk_thickness,
|
height=self.disk_thickness,
|
||||||
|
@ -995,6 +1060,7 @@ class DiskJoint(Model):
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
result.faces(">Z").tag("mate")
|
result.faces(">Z").tag("mate")
|
||||||
|
result.faces("<Z").tag("bot")
|
||||||
result.faces(">Z").workplane().tagPlane("dirX", direction="+X")
|
result.faces(">Z").workplane().tagPlane("dirX", direction="+X")
|
||||||
result = result.cut(
|
result = result.cut(
|
||||||
self
|
self
|
||||||
|
@ -1028,6 +1094,7 @@ class DiskJoint(Model):
|
||||||
)
|
)
|
||||||
theta = math.radians(carve_angle)
|
theta = math.radians(carve_angle)
|
||||||
result.faces("<Z").tag("mate")
|
result.faces("<Z").tag("mate")
|
||||||
|
result.faces(">Z").tag("top")
|
||||||
p_xy = result.copyWorkplane(Cq.Workplane('XY'))
|
p_xy = result.copyWorkplane(Cq.Workplane('XY'))
|
||||||
p_xy.tagPlane("dirX", direction="+X")
|
p_xy.tagPlane("dirX", direction="+X")
|
||||||
p_xy.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0))
|
p_xy.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0))
|
||||||
|
@ -1063,6 +1130,8 @@ class DiskJoint(Model):
|
||||||
housing_upper: str,
|
housing_upper: str,
|
||||||
disk: str,
|
disk: str,
|
||||||
angle: float = 0.0,
|
angle: float = 0.0,
|
||||||
|
fasteners: bool = True,
|
||||||
|
fastener_prefix: str = "fastener",
|
||||||
) -> Cq.Assembly:
|
) -> Cq.Assembly:
|
||||||
assert 0 <= angle <= self.movement_angle
|
assert 0 <= angle <= self.movement_angle
|
||||||
deflection = angle - (self.spring.angle_neutral - self.spring_angle_at_0)
|
deflection = angle - (self.spring.angle_neutral - self.spring_angle_at_0)
|
||||||
|
@ -1084,6 +1153,16 @@ class DiskJoint(Model):
|
||||||
#.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle)
|
#.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}?dirY", f"{disk}?dir", "Axis", param=angle - 90)
|
||||||
)
|
)
|
||||||
|
if fasteners:
|
||||||
|
tag_bolt = f"{fastener_prefix}_bolt"
|
||||||
|
tag_nut = f"{fastener_prefix}_nut"
|
||||||
|
(
|
||||||
|
assembly
|
||||||
|
.add(self.axle_bolt.assembly(), name=tag_bolt)
|
||||||
|
.add(self.axle_hex_nut.assembly(), name=tag_nut)
|
||||||
|
.constrain(f"{housing_lower}?bot", f"{tag_nut}?bot", "Plane")
|
||||||
|
.constrain(f"{housing_upper}?top", f"{tag_bolt}?root", "Plane", param=0)
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
assembly
|
assembly
|
||||||
)
|
)
|
||||||
|
@ -1136,7 +1215,9 @@ class ElbowJoint(Model):
|
||||||
|
|
||||||
lip_thickness: float = 5.0
|
lip_thickness: float = 5.0
|
||||||
lip_length: float = 60.0
|
lip_length: float = 60.0
|
||||||
hole_pos: list[float] = field(default_factory=lambda: [12, 24])
|
# Carve which allows light to go through
|
||||||
|
lip_side_depression_width: float = 10.0
|
||||||
|
hole_pos: list[float] = field(default_factory=lambda: [15, 24])
|
||||||
parent_arm_width: float = 10.0
|
parent_arm_width: float = 10.0
|
||||||
# Angle of the beginning of the parent arm
|
# Angle of the beginning of the parent arm
|
||||||
parent_arm_angle: float = 180.0
|
parent_arm_angle: float = 180.0
|
||||||
|
@ -1254,6 +1335,17 @@ class ElbowJoint(Model):
|
||||||
Hole(x=-sign * x, tag=f"conn_bot{i}")
|
Hole(x=-sign * x, tag=f"conn_bot{i}")
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
def post(sketch: Cq.Sketch) -> Cq.Sketch:
|
||||||
|
y_outer = self.disk_joint.total_thickness / 2
|
||||||
|
y_inner = self.disk_joint.tongue_thickness / 2
|
||||||
|
y = (y_outer + y_inner) / 2
|
||||||
|
width = self.lip_side_depression_width
|
||||||
|
height = y_outer - y_inner
|
||||||
|
return (
|
||||||
|
sketch
|
||||||
|
.push([(0, y), (0, -y)])
|
||||||
|
.rect(width, height, mode='s')
|
||||||
|
)
|
||||||
mbox = MountingBox(
|
mbox = MountingBox(
|
||||||
length=self.lip_length,
|
length=self.lip_length,
|
||||||
width=self.disk_joint.total_thickness,
|
width=self.disk_joint.total_thickness,
|
||||||
|
@ -1263,6 +1355,7 @@ class ElbowJoint(Model):
|
||||||
centred=(True, True),
|
centred=(True, True),
|
||||||
generate_side_tags=False,
|
generate_side_tags=False,
|
||||||
generate_reverse_tags=True,
|
generate_reverse_tags=True,
|
||||||
|
profile_callback=post,
|
||||||
)
|
)
|
||||||
return mbox.generate()
|
return mbox.generate()
|
||||||
|
|
||||||
|
@ -1332,7 +1425,7 @@ class ElbowJoint(Model):
|
||||||
def parent_joint_upper(self, generate_mount: bool=False, generate_tags=True):
|
def parent_joint_upper(self, generate_mount: bool=False, generate_tags=True):
|
||||||
axial_offset = Cq.Location((self.parent_arm_radius, 0, 0))
|
axial_offset = Cq.Location((self.parent_arm_radius, 0, 0))
|
||||||
housing_dz = self.disk_joint.housing_upper_dz
|
housing_dz = self.disk_joint.housing_upper_dz
|
||||||
conn_h = self.disk_joint.total_thickness
|
conn_h = self.disk_joint.tongue_thickness
|
||||||
conn_w = self.parent_arm_width
|
conn_w = self.parent_arm_width
|
||||||
connector = (
|
connector = (
|
||||||
Cq.Solid.makeBox(
|
Cq.Solid.makeBox(
|
||||||
|
|
|
@ -19,6 +19,7 @@ from nhf.touhou.houjuu_nue.electronics import (
|
||||||
LINEAR_ACTUATOR_21,
|
LINEAR_ACTUATOR_21,
|
||||||
LINEAR_ACTUATOR_50,
|
LINEAR_ACTUATOR_50,
|
||||||
ElectronicBoard,
|
ElectronicBoard,
|
||||||
|
LightStrip,
|
||||||
ELECTRONIC_MOUNT_HEXNUT,
|
ELECTRONIC_MOUNT_HEXNUT,
|
||||||
)
|
)
|
||||||
import nhf.utils
|
import nhf.utils
|
||||||
|
@ -52,6 +53,8 @@ class WingProfile(Model):
|
||||||
spacer_thickness: float = 25.4 / 4
|
spacer_thickness: float = 25.4 / 4
|
||||||
rod_width: float = 10.0
|
rod_width: float = 10.0
|
||||||
|
|
||||||
|
light_strip: LightStrip = LightStrip()
|
||||||
|
|
||||||
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
|
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
|
||||||
))
|
))
|
||||||
shoulder_angle_bias: float = 0.0
|
shoulder_angle_bias: float = 0.0
|
||||||
|
@ -370,9 +373,8 @@ class WingProfile(Model):
|
||||||
)
|
)
|
||||||
@submodel(name="spacer-s0-shoulder-act")
|
@submodel(name="spacer-s0-shoulder-act")
|
||||||
def spacer_s0_shoulder_act(self) -> MountingBox:
|
def spacer_s0_shoulder_act(self) -> MountingBox:
|
||||||
dx = self.shoulder_joint.draft_height
|
|
||||||
return MountingBox(
|
return MountingBox(
|
||||||
holes=[Hole(x=dx), Hole(x=-dx)],
|
holes=[Hole(x=0)],
|
||||||
hole_diam=self.shoulder_joint.actuator.back_hole_diam,
|
hole_diam=self.shoulder_joint.actuator.back_hole_diam,
|
||||||
length=self.root_height,
|
length=self.root_height,
|
||||||
width=10.0,
|
width=10.0,
|
||||||
|
@ -616,16 +618,17 @@ class WingProfile(Model):
|
||||||
Hole(sign * x, tag=tag)
|
Hole(sign * x, tag=tag)
|
||||||
for x, tag in joint.hole_loc_tags()
|
for x, tag in joint.hole_loc_tags()
|
||||||
]
|
]
|
||||||
|
tongue_thickness = joint.disk_joint.tongue_thickness
|
||||||
|
carve_width = joint.lip_side_depression_width
|
||||||
|
assert carve_width >= self.light_strip.width
|
||||||
|
carve_height = (segment_thickness - tongue_thickness) / 2
|
||||||
|
assert carve_height >= self.light_strip.height
|
||||||
def carve_sides(profile):
|
def carve_sides(profile):
|
||||||
dy = (segment_thickness + joint.total_thickness) / 4
|
dy = (segment_thickness + tongue_thickness) / 4
|
||||||
return (
|
return (
|
||||||
profile
|
profile
|
||||||
.push([(0,-dy), (0,dy)])
|
.push([(0,-dy), (0,dy)])
|
||||||
.rect(
|
.rect(carve_width, carve_height, mode='s')
|
||||||
joint.parent_arm_width,
|
|
||||||
(segment_thickness - joint.total_thickness) / 2,
|
|
||||||
mode='s',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
# FIXME: Carve out the sides so light can pass through
|
# FIXME: Carve out the sides so light can pass through
|
||||||
mbox = MountingBox(
|
mbox = MountingBox(
|
||||||
|
|
Loading…
Reference in New Issue