diff --git a/nhf/touhou/shiki_eiki/crown.py b/nhf/touhou/shiki_eiki/crown.py index 1896adf..8f9ef91 100644 --- a/nhf/touhou/shiki_eiki/crown.py +++ b/nhf/touhou/shiki_eiki/crown.py @@ -3,6 +3,7 @@ from nhf.build import Model, target, assembly, TargetKind import nhf.utils import math +from typing import Optional from dataclasses import dataclass, field from enum import Enum import cadquery as Cq @@ -11,6 +12,8 @@ class AttachPoint(Enum): DOVETAIL_IN = 1 DOVETAIL_OUT = 2 NONE = 3 + # Inset slot for front surface attachment j + SLOT = 4 @dataclass class Crown(Model): @@ -20,9 +23,15 @@ class Crown(Model): base_circ: float = 538.0 # Upper circumference, at the middle tilt_circ: float = 640.0 + front_base_circ: float = (640.0 + 538.0) / 2 # Total height height: float = 120.0 + # Front guard has a wing that inserts into the side guards. + front_wing_angle: float = 9.0 + front_wing_dh: float = 40.0 + front_wing_height: float = 20.0 + margin: float = 10.0 thickness: float = 0.4 # 26 Gauge @@ -33,6 +42,13 @@ class Crown(Model): side_guard_hole_diam: float = 1.5 side_guard_dovetail_height: float = 30.0 + side_guard_slot_width: float = 22.0 + side_guard_slot_angle: float = 18.0 + # brass insert thickness + slot_thickness: float = 2.0 + slot_width: float = 20.0 + slot_tilt: float = 45 + material: Material = Material.METAL_BRASS material_side: Material = Material.PLASTIC_PLA @@ -43,6 +59,9 @@ class Crown(Model): assert self.facet_width_upper / 2 > self.height / 2, "Top angle must be > 90 degrees" assert self.side_guard_channel_radius > self.radius_lower + assert self.front_wing_angle < 180 / self.facets + assert self.front_wing_dh + self.front_wing_height < self.height + @property def facet_width_lower(self): return self.base_circ / self.facets @@ -59,6 +78,16 @@ class Crown(Model): def radius_upper(self): return (self.tilt_circ + (self.tilt_circ - self.base_circ)) / (2 * math.pi) + @property + def radius_lower_front(self): + return self.front_base_circ / (2 * math.pi) + @property + def radius_middle_front(self): + return self.radius_lower_front + (self.radius_middle - self.radius_lower) + @property + def radius_upper_front(self): + return self.radius_lower_front + (self.radius_upper - self.radius_lower) + def profile_base(self) -> Cq.Sketch: # Generate the pentagonal shape @@ -383,12 +412,42 @@ class Crown(Model): (0,0,dx + self.side_guard_dovetail_height), ).moved((0, 0, -dx)) - def side_guard(self, attach_left: AttachPoint, attach_right: AttachPoint) -> Cq.Workplane: + def side_guard_frontal_slot(self) -> Cq.Workplane: + angle = 360 / self.facets + outer = Cq.Solid.makeCone( + radius1=self.radius_lower_front + self.thickness, + radius2=self.radius_upper_front + self.thickness, + height=self.height, + angleDegrees=angle, + ) + inner = Cq.Solid.makeCone( + radius1=self.radius_lower_front, + radius2=self.radius_upper_front, + height=self.height, + angleDegrees=angle, + ) + shell = ( + outer.cut(inner) + .rotate((0,0,0), (0,0,1), -angle/2) + ) + # Generate the sector intersector + intersector = Cq.Solid.makeCylinder( + radius=self.radius_upper + self.side_guard_thickness, + height=self.front_wing_height, + angleDegrees=self.front_wing_angle, + ).moved(Cq.Location(0,0,self.front_wing_dh,0,0,-self.front_wing_angle/2)) + return shell * intersector + + def side_guard( + self, + attach_left: AttachPoint, + attach_right: AttachPoint, + ) -> Cq.Workplane: """ Constructs the side guard using a cone. Via Gauss's Theorema Egregium, the surface of the cone can be deformed into a plane. """ - angle_span = 360 / 5 + angle_span = 360 / self.facets outer = Cq.Solid.makeCone( radius1=self.radius_lower + self.side_guard_thickness, radius2=self.radius_upper + self.side_guard_thickness, @@ -426,15 +485,20 @@ class Crown(Model): # ) #) result = shell * profile# - channel - for i in [-2, -1, 0, 1, 2]: - phi = i * (math.pi / 14) - hole = Cq.Solid.makeCylinder( - radius=self.side_guard_hole_diam / 2, - height=self.radius_upper * 2, - pnt=(0, 0, self.side_guard_hole_height), - dir=(math.cos(phi), math.sin(phi), 0), + + # Create the slots + for sign in [-1, 1]: + slot = Cq.Solid.makeBox( + length=self.height, + width=self.slot_width, + height=self.slot_thickness, + ).moved( + Cq.Location.rot2d(sign * self.side_guard_slot_angle) * + Cq.Location(self.radius_lower + self.side_guard_thickness/2, 0, 0) * + Cq.Location(0,0,0,0,-180 + self.slot_tilt,0) * + Cq.Location(-self.slot_thickness,-self.slot_width/2, -self.slot_thickness/2) ) - result = result - hole + result = result - slot radius_attach = self.radius_lower + self.side_guard_thickness / 2 # tilt the dovetail by radius differential @@ -443,12 +507,15 @@ class Crown(Model): loc_dovetail_left = Cq.Location.rot2d(angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0) loc_dovetail_right = Cq.Location.rot2d(-angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0) + angle_slot = 180 / self.facets - self.front_wing_angle / 2 match attach_left: case AttachPoint.DOVETAIL_IN: loc_dovetail_left *= Cq.Location.rot2d(180) result = result - dovetail.moved(loc_dovetail_left) case AttachPoint.DOVETAIL_OUT: result = result + dovetail.moved(loc_dovetail_left) + case AttachPoint.SLOT: + result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(angle_slot)) case AttachPoint.NONE: pass match attach_right: @@ -457,6 +524,8 @@ class Crown(Model): case AttachPoint.DOVETAIL_OUT: loc_dovetail_right *= Cq.Location.rot2d(180) result = result + dovetail.moved(loc_dovetail_right) + case AttachPoint.SLOT: + result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(-angle_slot)) case AttachPoint.NONE: pass # Remove parts below the horizontal @@ -466,6 +535,12 @@ class Crown(Model): height=cut_h).moved((0,0,-cut_h)) return result + @target(name="side_guard_1") + def side_guard_1(self) -> Cq.Workplane: + return self.side_guard( + attach_left=AttachPoint.SLOT, + attach_right=AttachPoint.DOVETAIL_IN, + ) @target(name="side_guard_2") def side_guard_2(self) -> Cq.Workplane: return self.side_guard( @@ -475,9 +550,15 @@ class Crown(Model): @target(name="side_guard_3") def side_guard_3(self) -> Cq.Workplane: return self.side_guard( - attach_left=AttachPoint.DOVETAIL_IN, + attach_left=AttachPoint.DOVETAIL_OUT, attach_right=AttachPoint.DOVETAIL_IN, ) + @target(name="side_guard_3") + def side_guard_4(self) -> Cq.Workplane: + return self.side_guard( + attach_left=AttachPoint.DOVETAIL_OUT, + attach_right=AttachPoint.SLOT, + ) def front_surrogate(self) -> Cq.Workplane: """ @@ -486,14 +567,14 @@ class Crown(Model): """ angle = 360 / 5 outer = Cq.Solid.makeCone( - radius1=self.radius_lower + self.thickness, - radius2=self.radius_upper + self.thickness, + radius1=self.radius_lower_front + self.thickness, + radius2=self.radius_upper_front + self.thickness, height=self.height, angleDegrees=angle, ) inner = Cq.Solid.makeCone( - radius1=self.radius_lower, - radius2=self.radius_upper, + radius1=self.radius_lower_front, + radius2=self.radius_upper_front, height=self.height, angleDegrees=angle, ) @@ -501,7 +582,7 @@ class Crown(Model): outer.cut(inner) .rotate((0,0,0), (0,0,1), -angle/2) ) - dx = math.sin(math.radians(angle / 2)) * self.radius_middle + dx = math.sin(math.radians(angle / 2)) * self.radius_middle_front profile = ( Cq.Workplane('YZ') .polyline([ @@ -512,7 +593,7 @@ class Crown(Model): (dx, self.height / 2), ]) .close() - .extrude(self.radius_upper + self.side_guard_thickness) + .extrude(self.radius_upper_front + self.side_guard_thickness) .val() ) return shell * profile @@ -521,14 +602,20 @@ class Crown(Model): """ New assembly using conformal mapping on the cone. """ - side_guard = self.side_guard_2() + side_guards = [ + self.side_guard_1(), + self.side_guard_2(), + self.side_guard_3(), + self.side_guard_4(), + ] a = Cq.Assembly() - for i in range(1,5): + for i,side_guard in enumerate(side_guards): + angle = -(i+1) * 360 / self.facets a = a.addS( side_guard, name=f"side-{i}", material=self.material_side, - loc=Cq.Location(rz=i*360/5) + loc=Cq.Location(rz=angle) ) a.addS( self.front_surrogate(),