import math from dataclasses import dataclass, field import cadquery as Cq from nhf import Material, Role from nhf.build import Model, target, assembly, TargetKind import nhf.utils @dataclass class Crown(Model): facets: int = 5 # Lower circumference base_circ: float = 538.0 # Upper circumference tilt_circ: float = 640.0 height: float = 120.0 margin: float = 10.0 thickness: float = 0.4 # 26 Gauge material: Material = Material.METAL_BRASS def __post_init__(self): super().__init__(name="crown") assert self.tilt_circ > self.base_circ assert self.facet_width_upper / 2 > self.height / 2, "Top angle must be > 90 degrees" @property def facet_width_lower(self): return self.base_circ / self.facets @property def facet_width_upper(self): return self.tilt_circ / self.facets def profile_base(self) -> Cq.Sketch: # Generate the pentagonal shape dx_l = self.facet_width_lower dx_u = self.facet_width_upper dy = self.height return ( Cq.Sketch() .polygon([ (dx_l/2, 0), (dx_u/2, dy/2), (0, dy), (-dx_u/2, dy/2), (-dx_l/2, 0), ]) ) @target(name="side", kind=TargetKind.DXF) def profile_side(self) -> Cq.Sketch: dy = self.facet_width_upper * 0.1 x_side = self.facet_width_upper y_tip = self.height - self.margin eye = ( Cq.Sketch() .segment( (0, y_tip), (dy, y_tip - dy), ) .segment( (0, y_tip), (-dy, y_tip - dy), ) .bezier([ (dy, y_tip - dy), (0, y_tip - dy/2), (0, y_tip - dy/2), (-dy, y_tip - dy), ]) .assemble() ) return ( self.profile_base() .boolean(eye, mode='s') ) @target(name="dot", kind=TargetKind.DXF) def profile_dot(self) -> Cq.Sketch: return ( Cq.Sketch() .circle(self.margin / 2) ) @target(name="front", kind=TargetKind.DXF) def profile_front(self) -> Cq.Sketch: dx_l = self.facet_width_lower dx_u = self.facet_width_upper dy = self.height window_length = dy / 5 window_height = self.margin / 2 window = ( Cq.Sketch() .rect(window_length, window_height) ) window_p1 = Cq.Location.from2d( dx_u/2 - self.margin - window_length * 0.4, dy/2 + self.margin/2, math.degrees(math.atan2(dy/2, -dx_u/2)), ) window_p2 = Cq.Location.from2d( dx_l/2 - self.margin + window_length * 0.15, window_length/2 + self.margin, math.degrees(math.atan2(dy/2, (dx_u-dx_l)/2)), ) # Carve the scale z = dy * 1/64 # "Pen" Thickness scale_pan_x = dx_l / 2 * 0.6 scale_pan_y = dy / 2 * 0.7 pan_dx = dx_l * 1/4 pan_dy = dy * 1/16 scale_pan = ( Cq.Sketch() .arc( (- pan_dx/2, pan_dy), (0, 0), (+ pan_dx/2, pan_dy), ) .segment( (+pan_dx/2, pan_dy), (+pan_dx/2 - z, pan_dy), ) .arc( (-pan_dx/2 + z, pan_dy), (0, z), (+pan_dx/2 - z, pan_dy), ) .segment( (-pan_dx/2, pan_dy), (-pan_dx/2 + z, pan_dy), ) .assemble() ) loc_scale_pan = Cq.Location.from2d(scale_pan_x, scale_pan_y) loc_scale_pan2 = Cq.Location.from2d(-scale_pan_x, scale_pan_y) scale_base_y = dy / 2 * 0.36 scale_base_x = dx_l / 10 assert scale_base_y < scale_pan_y assert scale_base_x < scale_pan_x scale_body = ( Cq.Sketch() .arc( (scale_pan_x, scale_pan_y), (0, scale_base_y), (-scale_pan_x, scale_pan_y), ) .segment( (-scale_pan_x, scale_pan_y), (-scale_pan_x+z, scale_pan_y+z), ) .arc( (scale_pan_x - z, scale_pan_y+z), (0, scale_base_y + z), (-scale_pan_x + z, scale_pan_y+z), ) .segment( (scale_pan_x, scale_pan_y), (scale_pan_x-z, scale_pan_y+z), ) .assemble() .polygon([ (scale_base_x, scale_base_y + z/2), (scale_base_x, self.margin), (scale_base_x-z, self.margin), (scale_base_x-z, scale_base_y-z), (-scale_base_x+z, scale_base_y-z), (-scale_base_x+z, self.margin), (-scale_base_x, self.margin), (-scale_base_x, scale_base_y + z/2), ], mode='a') ) # Needle needle_y_top = dy - self.margin needle_y_mid = dy * 0.7 needle_dx = scale_base_x * 2 y_shoulder = needle_y_mid - z * 2 needle = ( Cq.Sketch() .segment( (0, needle_y_mid), (z, y_shoulder), ) .segment( (z, y_shoulder), (z, scale_base_y), ) .segment( (z, scale_base_y), (-z, scale_base_y), ) .segment( (-z, y_shoulder), (-z, scale_base_y), ) .segment( (-z, y_shoulder), (0, needle_y_mid), ) .assemble() ) z2 = z * 2 y1 = needle_y_mid + z2 needle_head = ( Cq.Sketch() .segment( (z, needle_y_mid), (z, y1), ) .segment( (-z, needle_y_mid), (-z, y1), ) # Outer edge .bezier([ (0, needle_y_top), (0, (needle_y_top + needle_y_mid)/2), (needle_dx, (needle_y_top + needle_y_mid)/2), (z, needle_y_mid), ]) .bezier([ (0, needle_y_top), (0, (needle_y_top + needle_y_mid)/2), (-needle_dx, (needle_y_top + needle_y_mid)/2), (-z, needle_y_mid), ]) # Inner edge .bezier([ (0, needle_y_top - z2), (0, (needle_y_top + needle_y_mid)/2), (needle_dx-z2*2, (needle_y_top + needle_y_mid)/2), (z, y1), ]) .bezier([ (0, needle_y_top - z2), (0, (needle_y_top + needle_y_mid)/2), (-needle_dx+z2*2, (needle_y_top + needle_y_mid)/2), (-z, y1), ]) .assemble() ) return ( self.profile_base() .boolean(window.moved(window_p1), mode='s') .boolean(window.moved(window_p1.flip_x()), mode='s') .boolean(window.moved(window_p2), mode='s') .boolean(window.moved(window_p2.flip_x()), mode='s') .boolean(scale_pan.moved(loc_scale_pan), mode='s') .boolean(scale_pan.moved(loc_scale_pan2), mode='s') .boolean(scale_body, mode='s') .boolean(needle, mode='s') .boolean(needle_head, mode='s') .clean() ) @target(name="side-guard", kind=TargetKind.DXF) def profile_side_guard(self) -> Cq.Sketch: dx = self.facet_width_lower / 2 dy = self.height # Main control points p_mid = Cq.Location.from2d(0, 0.5 * dy) p_mid_v = Cq.Location.from2d(10/57 * dx, 0) p_top1 = Cq.Location.from2d(0.408 * dx, 5/24 * dy) p_top1_v = Cq.Location.from2d(0.13 * dx, 0) p_top2 = Cq.Location.from2d(0.737 * dx, 0.255 * dy) p_top2_c1 = p_top2 * Cq.Location.from2d(-0.105 * dx, 0.033 * dy) p_top2_c2 = p_top2 * Cq.Location.from2d(-0.053 * dx, -0.09 * dy) p_top3 = Cq.Location.from2d(0.929 * dx, 0.145 * dy) p_top3_v = Cq.Location.from2d(0.066 * dx, 0.033 * dy) p_top4 = Cq.Location.from2d(0.85 * dx, 0.374 * dy) p_top4_v = Cq.Location.from2d(-0.053 * dx, 0.008 * dy) p_top5 = Cq.Location.from2d(0.54 * dx, 0.349 * dy) p_top5_c1 = p_top5 * Cq.Location.from2d(0.103 * dx, 0.017 * dy) p_top5_c2 = p_top5 * Cq.Location.from2d(0.158 * dx, 0.034 * dy) p_base_c = Cq.Location.from2d(1.245 * dx, 0.55 * dy) p_base = Cq.Location.from2d(dx, 0) bezier_groups = [ [ p_base, p_base_c, p_top5_c2, p_top5, ], [ p_top5, p_top5_c1, p_top4 * p_top4_v, p_top4, ], [ p_top4, p_top4 * p_top4_v.inverse.scale(4), p_top3 * p_top3_v, p_top3, ], [ p_top3, p_top3 * p_top3_v.inverse, p_top2_c2, p_top2, ], [ p_top2, p_top2_c1, p_top1 * p_top1_v, p_top1, ], [ p_top1, p_top1 * p_top1_v.inverse, p_mid * p_mid_v, p_mid, ], ] sketch = ( Cq.Sketch() .segment( p_base.to2d_pos(), p_base.flip_x().to2d_pos(), ) ) for bezier_group in bezier_groups: sketch = ( sketch .bezier([p.to2d_pos() for p in bezier_group]) .bezier([p.flip_x().to2d_pos() for p in bezier_group]) ) return sketch.assemble() def assembly(self) -> Cq.Assembly: front = ( Cq.Workplane('XY') .placeSketch(self.profile_front()) .extrude(self.thickness) ) side = ( Cq.Workplane('XY') .placeSketch(self.profile_side()) .extrude(self.thickness) ) side_guard = ( Cq.Workplane('XY') .placeSketch(self.profile_side_guard()) .extrude(self.thickness) ) assembly = ( Cq.Assembly() .addS( front, name="front", material=self.material, role=Role.DECORATION, ) ) for i, pos in enumerate([-2, -1, 1, 2]): x = self.facet_width_upper * pos assembly = ( assembly .addS( side, name=f"side{i}", material=self.material, role=Role.DECORATION, loc=Cq.Location.from2d(x, 0), ) .addS( side_guard, name=f"guard{i}", material=self.material, role=Role.DECORATION, loc=Cq.Location(x, 0, self.thickness), ) ) return assembly