""" This file describes the shapes of the wing shells. The joints are defined in `__init__.py`. """ import math from enum import Enum 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, TargetKind, target, assembly, submodel 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, DiskJoint from nhf.parts.planar import extrude_with_markers import nhf.utils @dataclass(kw_only=True) class WingProfile(Model): name: str = "wing" base_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=25.0, radius_inner=15.0, tooth_height=7.0, base_height=5, n_tooth=24, )) base_width: float = 80.0 hs_joint_corner_dx: float = 17.0 hs_joint_corner_dz: float = 24.0 hs_joint_corner_hole_diam: float = 6.0 hs_joint_axis_diam: float = 12.0 base_plate_width: float = 50.0 panel_thickness: float = 25.4 / 16 spacer_thickness: float = 25.4 / 8 shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( )) shoulder_angle_bias: float = 0.0 shoulder_width: float = 36.0 shoulder_tip_x: float = -200.0 shoulder_tip_y: float = 160.0 shoulder_mid_x: float = -105.0 shoulder_mid_y: float = 75.0 s1_thickness: float = 25.0 elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( movement_angle=55, ), hole_diam=6.0, )) # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 s2_thickness: float = 25.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( movement_angle=45, radius_disk=13.0, radius_housing=15.0, ), hole_pos=[10, 20], lip_length=50, child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 s3_thickness: float = 25.0 mat_panel: Material = Material.ACRYLIC_TRANSLUSCENT mat_bracket: Material = Material.ACRYLIC_TRANSPARENT mat_hs_joint: Material = Material.PLASTIC_PLA role_panel: Role = Role.STRUCTURE # Subclass must populate elbow_bot_loc: Cq.Location elbow_height: float wrist_bot_loc: Cq.Location wrist_height: float flip: bool = False def __post_init__(self): super().__init__(name=self.name) self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: return self.shoulder_joint @submodel(name="elbow-joint") def submodel_elbow_joint(self) -> Model: return self.elbow_joint @submodel(name="wrist-joint") def submodel_wrist_joint(self) -> Model: return self.wrist_joint @property def root_height(self) -> float: return self.shoulder_joint.height @property def shoulder_height(self) -> float: return self.shoulder_joint.height @target(name="base-hs-joint") def base_hs_joint(self) -> Cq.Workplane: """ Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth coupling, a cylindrical base, and a mounting base. """ hirth = self.base_joint.generate(is_mated=True) dy = self.hs_joint_corner_dx dx = self.hs_joint_corner_dz conn = [ (-dx, -dy), (dx, -dy), (dx, dy), (-dx, dy), ] result = ( Cq.Workplane('XY') .box( self.root_height, self.base_plate_width, self.base_joint.base_height, centered=(True, True, False)) #.translate((0, 0, -self.base_joint.base_height)) #.edges("|Z") #.fillet(self.hs_joint_corner_fillet) .faces(">Z") .workplane() .pushPoints(conn) .hole(self.hs_joint_corner_hole_diam) ) # Creates a plane parallel to the holes but shifted to the base plane = result.faces(">Z").workplane(offset=-self.base_joint.base_height) for i, (px, py) in enumerate(conn): plane.moveTo(px, py).tagPlane(f"conn{i}") result = ( result .faces(">Z") .workplane() .union(hirth, tol=0.1) .clean() ) result = ( result.faces(" Cq.Sketch: """ The outer boundary of s0, used to produce the curved panel and the top/bottom slots """ tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y return ( Cq.Sketch() .spline([ (0, 0), (-30.0, 80.0), (tip_x, tip_y) ]) #.segment( # (tip_x, tip_y), # (tip_x - 10, tip_y), #) ) @property def shoulder_angle_neutral(self) -> float: """ Returns the neutral angle of the shoulder """ dx = self.shoulder_mid_x - self.shoulder_tip_x dy = -(self.shoulder_mid_y - (self.shoulder_tip_y - self.shoulder_width)) result = math.degrees(math.atan2(dy, dx)) assert result >= 0 return result @target(name="profile-s0", kind=TargetKind.DXF) def profile_s0(self) -> Cq.Sketch: tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y mid_x = self.shoulder_mid_x mid_y = self.shoulder_mid_y sw = self.shoulder_width sketch = ( self.outer_profile_s0() .segment((-self.base_width, 0), (0, 0)) .segment( (tip_x, tip_y), (tip_x, tip_y - sw), ) .segment( (tip_x, tip_y - sw), (mid_x, mid_y), ) .segment( (mid_x, mid_y), (-self.base_width, 0), ) .assemble() ) return sketch def outer_shell_s0(self) -> Cq.Workplane: t = self.panel_thickness profile = Cq.Wire.assembleEdges(self.outer_profile_s0().edges().vals()) result = ( Cq.Workplane('XZ') .rect(t, self.root_height + t*2, centered=(False, False)) .sweep(profile) ) plane = result.copyWorkplane(Cq.Workplane('XZ')) plane.moveTo(0, 0).tagPlane("bot") plane.moveTo(0, self.root_height + t*2).tagPlane("top") return result @submodel(name="spacer-s0-shoulder") def spacer_s0_shoulder(self) -> MountingBox: """ Should be cut """ holes = [ hole for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos) for hole in [ Hole(x=x, y=y, tag=f"conn_top{i}"), Hole(x=-x, y=y, tag=f"conn_bot{i}"), ] ] return MountingBox( length=self.shoulder_joint.height, width=self.shoulder_joint.parent_lip_width, thickness=self.spacer_thickness, holes=holes, hole_diam=self.shoulder_joint.parent_conn_hole_diam, centred=(True, True), flip_y=self.flip, ) @submodel(name="spacer-s0-shoulder") def spacer_s0_base(self) -> MountingBox: """ Should be cut """ assert self.base_plate_width < self.base_width assert self.hs_joint_corner_dx * 2 < self.base_width assert self.hs_joint_corner_dz * 2 < self.root_height dy = self.hs_joint_corner_dx dx = self.hs_joint_corner_dz holes = [ Hole(x=-dx, y=-dy), Hole(x=dx, y=-dy), Hole(x=dx, y=dy), Hole(x=-dx, y=dy), ] return MountingBox( length=self.root_height, width=self.base_plate_width, thickness=self.spacer_thickness, holes=holes, hole_diam=self.hs_joint_corner_hole_diam, centred=(True, True), flip_y=self.flip, ) def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.base_plate_width) / 2 base_dy = self.base_joint.joint_height sw = self.shoulder_width axle_dist = self.shoulder_joint.parent_lip_ext theta = math.radians(self.shoulder_angle_neutral) c, s = math.cos(theta), math.sin(theta) tags = [ # transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2) ("shoulder", Cq.Location.from2d( self.shoulder_tip_x + axle_dist * c + (-sw/2) * s, self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c, -self.shoulder_angle_neutral)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ] result = extrude_with_markers( self.profile_s0(), self.panel_thickness, tags, reverse=not top, ) h = self.panel_thickness if top else 0 result.copyWorkplane(Cq.Workplane('XZ')).moveTo(0, h).tagPlane("corner") return result @assembly() def assembly_s0(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s0(top=True), name="bot", material=self.mat_panel, role=self.role_panel) .addS(self.surface_s0(top=False), name="top", material=self.mat_panel, role=self.role_panel, loc=Cq.Location((0, 0, self.root_height + self.panel_thickness))) .constrain("bot", "Fixed") .constrain("top", "Fixed") #.constrain("bot@faces@>Z", "top@faces@ Cq.Sketch: """ Generates profile from shoulder and above """ 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, rotate: 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 = 180 if rotate else 0 ( a .addS( spacer, name=point_tag, material=self.mat_bracket, role=self.role_panel) .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) ) def _mask_elbow(self) -> list[Tuple[float, float]]: """ Polygon shape to mask out parts above the elbow """ def _mask_wrist(self) -> list[Tuple[float, float]]: """ Polygon shape to mask wrist """ def spacer_of_joint( self, joint: ElbowJoint, segment_thickness: float, dx: float, bot=False) -> MountingBox: length = joint.lip_length / 2 - dx holes = [ Hole(x - dx) for x in joint.hole_pos ] mbox = MountingBox( length=length, width=segment_thickness, thickness=self.spacer_thickness, holes=holes, hole_diam=joint.hole_diam, centred=(False, True), ) return mbox @target(name="profile-s1", kind=TargetKind.DXF) def profile_s1(self) -> Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_elbow(), mode='i') ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: shoulder_h = self.shoulder_joint.child_height h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ ("shoulder_bot", Cq.Location.from2d(0, h, 90)), ("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), ] h = self.elbow_height / 2 tags_elbow = [ ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( -self.elbow_joint.parent_arm_radius, h - self.elbow_h2)), ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( -self.elbow_joint.parent_arm_radius, h + self.elbow_h2)), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow return extrude_with_markers( profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s1-shoulder") 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, ) @submodel(name="spacer-s1-elbow") def spacer_s1_elbow(self) -> MountingBox: return self.spacer_of_joint( joint=self.elbow_joint, segment_thickness=self.s1_thickness, dx=self.elbow_h2, ) @assembly() def assembly_s1(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s1(front=True), name="front", material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") .addS(self.surface_s1(front=False), name="back", material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_elbow(), mode='s') .reset() .polygon(self._mask_wrist(), mode='i') ) return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 tags_elbow = [ ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( self.elbow_joint.child_arm_radius, h - self.elbow_h2, 180)), ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( self.elbow_joint.child_arm_radius, h + self.elbow_h2, 180)), ] h = self.wrist_height / 2 tags_wrist = [ ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( -self.wrist_joint.parent_arm_radius, h - self.wrist_h2)), ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( -self.wrist_joint.parent_arm_radius, h + self.wrist_h2)), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s2-elbow") def spacer_s2_elbow(self) -> MountingBox: return self.spacer_of_joint( joint=self.elbow_joint, segment_thickness=self.s2_thickness, dx=self.elbow_h2, ) @submodel(name="spacer-s2-wrist") def spacer_s2_wrist(self) -> MountingBox: return self.spacer_of_joint( joint=self.wrist_joint, segment_thickness=self.s2_thickness, dx=self.wrist_h2, ) @assembly() def assembly_s2(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s2(front=True), name="front", material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") .addS(self.surface_s2(front=False), name="back", material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_wrist(), mode='s') ) return profile def surface_s3(self, front: bool = True) -> Cq.Workplane: h = self.wrist_height / 2 tags = [ ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( self.wrist_joint.child_arm_radius, h - self.wrist_h2, 180)), ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( self.wrist_joint.child_arm_radius, h + self.wrist_h2, 180)), ] profile = self.profile_s3() return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s3-wrist") def spacer_s3_wrist(self) -> MountingBox: return self.spacer_of_joint( joint=self.wrist_joint, segment_thickness=self.s3_thickness, dx=self.wrist_h2, ) @assembly() def assembly_s3(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s3(front=True), name="front", material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") .addS(self.surface_s3(front=False), name="back", material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Assembly(): if parts is None: parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] result = ( Cq.Assembly() ) tag_top, tag_bot = "top", "bot" if self.flip: tag_top, tag_bot = tag_bot, tag_top 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(f"s0/shoulder?conn_top0", f"shoulder/parent_{tag_top}/lip?conn0", "Plane") .constrain(f"s0/shoulder?conn_top1", f"shoulder/parent_{tag_top}/lip?conn1", "Plane") .constrain(f"s0/shoulder?conn_bot0", f"shoulder/parent_{tag_bot}/lip?conn0", "Plane") .constrain(f"s0/shoulder?conn_bot1", f"shoulder/parent_{tag_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", f"shoulder/child/lip_{tag_top}?conn0", "Plane") .constrain("s1/shoulder_top?conn1", f"shoulder/child/lip_{tag_top}?conn1", "Plane") .constrain("s1/shoulder_bot?conn0", f"shoulder/child/lip_{tag_bot}?conn0", "Plane") .constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane") ) if "elbow" in parts: result.add(self.elbow_joint.assembly(angle=angle_elbow_wrist), name="elbow") if "s1" in parts and "elbow" in parts: ( result .constrain("s1/elbow_top?conn0", "elbow/parent_upper/lip?conn_top0", "Plane") .constrain("s1/elbow_top?conn1", "elbow/parent_upper/lip?conn_top1", "Plane") .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/lip?conn_bot0", "Plane") .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/lip?conn_bot1", "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/lip?conn_top0", "Plane") .constrain("s2/elbow_top?conn1", "elbow/child/lip?conn_top1", "Plane") .constrain("s2/elbow_bot?conn0", "elbow/child/lip?conn_bot0", "Plane") .constrain("s2/elbow_bot?conn1", "elbow/child/lip?conn_bot1", "Plane") ) if "wrist" in parts: 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/lip?conn_bot0", "Plane") .constrain("s2/wrist_top?conn1", "wrist/parent_upper/lip?conn_bot1", "Plane") .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/lip?conn_top0", "Plane") .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/lip?conn_top1", "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/lip?conn_bot0", "Plane") .constrain("s3/wrist_top?conn1", "wrist/child/lip?conn_bot1", "Plane") .constrain("s3/wrist_bot?conn0", "wrist/child/lip?conn_top0", "Plane") .constrain("s3/wrist_bot?conn1", "wrist/child/lip?conn_top1", "Plane") ) if len(parts) > 1: result.solve() return result @dataclass(kw_only=True) class WingR(WingProfile): """ Right side wings """ elbow_bot_loc: Cq.Location = Cq.Location.from2d(285.0, 5.0, 25.0) elbow_height: float = 111.0 wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 253.0, 40.0) wrist_height: float = 60.0 # Extends from the wrist to the tip of the arrow arrow_height: float = 300 arrow_angle: float = -8 # Relative (in wrist coordinate) centre of the ring ring_rel_loc: Cq.Location = Cq.Location.from2d(45.0, 25.0) ring_radius_inner: float = 22.0 def __post_init__(self): super().__post_init__() assert self.arrow_angle < 0, "Arrow angle cannot be positive" self.arrow_bot_loc = self.wrist_bot_loc \ * Cq.Location.from2d(0, -self.arrow_height) self.arrow_other_loc = self.arrow_bot_loc \ * Cq.Location.rot2d(self.arrow_angle) \ * Cq.Location.from2d(0, self.arrow_height + self.wrist_height) self.ring_loc = self.wrist_top_loc * self.ring_rel_loc assert self.ring_radius > self.ring_radius_inner @property def ring_radius(self) -> float: (dx, dy), _ = self.ring_rel_loc.to2d() return (dx * dx + dy * dy) ** 0.5 def profile(self) -> Cq.Sketch: """ Net profile of the wing starting from the wing root with no divisions """ result = ( Cq.Sketch() .segment( (0, 0), (0, self.shoulder_joint.height), tag="shoulder") .spline([ (0, self.shoulder_joint.height), self.elbow_top_loc.to2d_pos(), self.wrist_top_loc.to2d_pos(), ], tag="s1_top") #.segment( # (self.wrist_x, self.wrist_y), # (wrist_top_x, wrist_top_y), # tag="wrist") .spline([ (0, 0), self.elbow_bot_loc.to2d_pos(), self.wrist_bot_loc.to2d_pos(), ], tag="s1_bot") ) result = ( result .segment( self.wrist_bot_loc.to2d_pos(), self.arrow_bot_loc.to2d_pos(), ) .segment( self.arrow_bot_loc.to2d_pos(), self.arrow_other_loc.to2d_pos(), ) .segment( self.arrow_other_loc.to2d_pos(), self.wrist_top_loc.to2d_pos(), ) ) # Carve out the ring result = result.assemble() result = ( result .push([self.ring_loc.to2d_pos()]) .circle(self.ring_radius, mode='a') .circle(self.ring_radius_inner, mode='s') .clean() ) return result def _mask_elbow(self) -> list[Tuple[float, float]]: l = 200 elbow_x, _ = self.elbow_bot_loc.to2d_pos() elbow_top_x, _ = self.elbow_top_loc.to2d_pos() return [ (0, -l), (elbow_x, -l), self.elbow_bot_loc.to2d_pos(), self.elbow_top_loc.to2d_pos(), (elbow_top_x, l), (0, l) ] def _mask_wrist(self) -> list[Tuple[float, float]]: l = 200 wrist_x, _ = self.wrist_bot_loc.to2d_pos() _, wrist_top_y = self.wrist_top_loc.to2d_pos() return [ (0, -l), (wrist_x, -l), self.wrist_bot_loc.to2d_pos(), self.wrist_top_loc.to2d_pos(), #(self.wrist_top_x, self.wrist_top_y), (0, wrist_top_y), ] @dataclass(kw_only=True) class WingL(WingProfile): elbow_bot_loc: Cq.Location = Cq.Location.from2d(230.0, 110.0, -10.0) elbow_height: float = 80.0 wrist_angle: float = -45.0 wrist_bot_loc: Cq.Location = Cq.Location.from2d(480.0, 0.0, -45.0) wrist_height: float = 43.0 shoulder_bezier_ext: float = 80.0 elbow_bezier_ext: float = 100.0 wrist_bezier_ext: float = 30.0 arrow_length: float = 135.0 arrow_height: float = 120.0 flip: bool = True def __post_init__(self): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) super().__post_init__() def arrow_to_abs(self, x, y) -> Tuple[float, float]: rel = Cq.Location.from2d(x * self.arrow_length, y * self.arrow_height / 2 + self.wrist_height / 2) return (self.wrist_bot_loc * rel).to2d_pos() def profile(self) -> Cq.Sketch: result = ( Cq.Sketch() .segment( (0,0), (0, self.shoulder_height) ) .bezier([ (0, 0), (self.shoulder_bezier_ext, 0), (self.elbow_bot_loc * Cq.Location.from2d(-self.elbow_bezier_ext, 0)).to2d_pos(), self.elbow_bot_loc.to2d_pos(), ]) .bezier([ (0, self.shoulder_joint.height), (self.shoulder_bezier_ext, self.shoulder_joint.height), (self.elbow_top_loc * Cq.Location.from2d(-self.elbow_bezier_ext, 0)).to2d_pos(), self.elbow_top_loc.to2d_pos(), ]) .bezier([ self.elbow_bot_loc.to2d_pos(), (self.elbow_bot_loc * Cq.Location.from2d(self.elbow_bezier_ext, 0)).to2d_pos(), (self.wrist_bot_loc * Cq.Location.from2d(-self.wrist_bezier_ext, 0)).to2d_pos(), self.wrist_bot_loc.to2d_pos(), ]) .bezier([ self.elbow_top_loc.to2d_pos(), (self.elbow_top_loc * Cq.Location.from2d(self.elbow_bezier_ext, 0)).to2d_pos(), (self.wrist_top_loc * Cq.Location.from2d(-self.wrist_bezier_ext, 0)).to2d_pos(), self.wrist_top_loc.to2d_pos(), ]) ) # arrow base positions base_u, base_v = 0.3, 0.3 result = ( result .bezier([ self.wrist_top_loc.to2d_pos(), (self.wrist_top_loc * Cq.Location.from2d(self.wrist_bezier_ext, 0)).to2d_pos(), self.arrow_to_abs(base_u, base_v), ]) .bezier([ self.wrist_bot_loc.to2d_pos(), (self.wrist_bot_loc * Cq.Location.from2d(self.wrist_bezier_ext, 0)).to2d_pos(), self.arrow_to_abs(base_u, -base_v), ]) ) # Create the arrow arrow_beziers = [ [ (0, 1), (0.3, 1), (0.8, .2), (1, 0), ], [ (0, 1), (0.1, 0.8), (base_u, base_v), ] ] arrow_beziers = [ l2 for l in arrow_beziers for l2 in [l, [(x, -y) for x,y in l]] ] for line in arrow_beziers: result = result.bezier([self.arrow_to_abs(x, y) for x,y in line]) return result.assemble() def _mask_elbow(self) -> list[Tuple[float, float]]: l = 200 elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos() elbow_top_x, _ = self.elbow_top_loc.to2d_pos() return [ (0, -l), (elbow_bot_x, -l), self.elbow_bot_loc.to2d_pos(), self.elbow_top_loc.to2d_pos(), (elbow_top_x, l), (0, l) ] def _mask_wrist(self) -> list[Tuple[float, float]]: l = 200 elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos() _, elbow_top_y = self.elbow_top_loc.to2d_pos() _, wrist_bot_y = self.wrist_bot_loc.to2d_pos() return [ (0, -l), (elbow_bot_x, wrist_bot_y), self.wrist_bot_loc.to2d_pos(), self.wrist_top_loc.to2d_pos(), (elbow_bot_x, elbow_top_y + l), (0, l), ]