diff --git a/nhf/touhou/houjuu_nue/parts.py b/nhf/touhou/houjuu_nue/parts.py new file mode 100644 index 0000000..a3a5bfb --- /dev/null +++ b/nhf/touhou/houjuu_nue/parts.py @@ -0,0 +1,220 @@ +from dataclasses import dataclass +import cadquery as Cq +from nhf import Role +from nhf.build import Model, target +import nhf.parts.springs as springs +import nhf.utils + +TOL = 1e-6 + +@dataclass +class DiskJoint(Model): + """ + Sandwiched disk joint for the wrist and elbow + """ + + 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: float = 90.0 + spring_angle_shift: float = 0 + wall_inset: float = 2.0 + + # Angular span of movement + movement_angle: float = 120.0 + # Angular span of tongue on disk + tongue_span: float = 30.0 + tongue_length: float = 10.0 + + generate_inner_wall: bool = False + + + def __post_init__(self): + super().__init__(name="disk-joint") + assert self.housing_thickness > self.wall_inset + assert self.radius_housing > self.radius_disk + 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 total_thickness(self): + return self.housing_thickness * 2 + self.disk_thickness + + @property + def opening_span(self): + return self.movement_angle + self.tongue_span + + @property + def housing_upper_carve_offset(self): + return self.housing_thickness + self.disk_thickness - self.spring_height + + @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, + height=self.disk_thickness, + ) + .located(Cq.Location((0, self.radius_spring_internal, 0))) + .rotate((0, 0, 0), (0, 0, 1), self.spring_angle_shift) + ) + 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( + height=self.disk_thickness, + radius=radius_tongue, + angleDegrees=self.tongue_span, + ).cut(Cq.Solid.makeCylinder( + height=self.disk_thickness, + radius=self.radius_disk, + )) + ) + result = result.union(tongue, tol=TOL) + return result + + def wall(self) -> Cq.Compound: + height = self.disk_thickness + self.wall_inset + wall = Cq.Solid.makeCylinder( + radius=self.radius_housing, + height=height, + angleDegrees=360 - self.opening_span, + ).cut(Cq.Solid.makeCylinder( + radius=self.radius_disk, + height=height, + )).rotate((0, 0, 0), (0, 0, 1), self.opening_span) + return wall + + @target(name="housing-lower") + def housing_lower(self) -> Cq.Workplane: + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.radius_housing, + height=self.housing_thickness, + centered=(True, True, False), + ) + .cut(Cq.Solid.makeCylinder( + radius=self.radius_axle, + height=self.housing_thickness, + )) + ) + result.faces(">Z").tag("mate") + result.faces(">Z").workplane().tagPlane("dir", direction="+X") + result = result.cut( + self + .wall() + .mirror("XY") + .located(Cq.Location((0, 0, self.housing_thickness + self.disk_thickness))) + #.rotate((0, 0, 0), (1, 0, 0), 180) + #.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness))) + ) + return result + + @target(name="housing-upper") + def housing_upper(self) -> Cq.Workplane: + carve = ( + Cq.Solid.makeCylinder( + radius=self.radius_spring, + height=self.housing_thickness + ).fuse(Cq.Solid.makeBox( + length=self.spring_tail_length, + width=self.spring_thickness, + height=self.housing_thickness + ).located(Cq.Location((0, self.radius_spring_internal, 0)))) + ).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift) + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.radius_housing, + height=self.housing_thickness, + centered=(True, True, False), + ) + ) + result.faces(">Z").tag("mate") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + result = result.faces(">Z").hole(self.radius_axle * 2) + + # tube which holds the spring interior + if self.generate_inner_wall: + tube = ( + Cq.Solid.makeCylinder( + radius=self.radius_spring_internal, + height=self.disk_thickness + self.housing_thickness, + ).cut(Cq.Solid.makeCylinder( + radius=self.radius_axle, + height=self.disk_thickness + self.housing_thickness, + )) + ) + result = result.union(tube) + wall = ( + self.wall() + .rotate((0, 0, 0), (1, 0, 0), 180) + .located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset))) + ) + result = ( + result + .union(wall, tol=TOL) + .cut(carve.located(Cq.Location((0, 0, self.housing_upper_carve_offset)))) + ) + return result + + + def assembly(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.disk(), name="disk", color=Role.CHILD.color) + .add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color) + .add(self.housing_upper(), name="housing_upper", color=Role.PARENT.color) + .constrain("disk?mate_bot", "housing_lower?mate", "Plane") + .constrain("disk?mate_top", "housing_upper?mate", "Plane") + .solve() + ) + return result + +if __name__ == '__main__': + p = DiskJoint() + p.build_all()