diff --git a/nhf/touhou/shiki_eiki/__init__.py b/nhf/touhou/shiki_eiki/__init__.py index 034bd7d..ee4ccbe 100644 --- a/nhf/touhou/shiki_eiki/__init__.py +++ b/nhf/touhou/shiki_eiki/__init__.py @@ -2,12 +2,16 @@ from dataclasses import dataclass, field import cadquery as Cq from nhf.build import Model, TargetKind, target, assembly, submodel import nhf.touhou.shiki_eiki.rod as MR +import nhf.touhou.shiki_eiki.crown as MC +import nhf.touhou.shiki_eiki.epaulette as ME import nhf.utils @dataclass class Parameters(Model): rod: MR.Rod = field(default_factory=lambda: MR.Rod()) + crown: MC.Crown = field(default_factory=lambda: MC.Crown()) + epaulette: ME.Epaulette = field(default_factory=lambda: ME.Epaulette()) def __post_init__(self): super().__init__(name="shiki-eiki") @@ -15,6 +19,12 @@ class Parameters(Model): @submodel(name="rod") def submodel_rod(self) -> Model: return self.rod + @submodel(name="crown") + def submodel_crown(self) -> Model: + return self.crown + @submodel(name="epaulette") + def submodel_epaulette(self) -> Model: + return self.epaulette if __name__ == '__main__': diff --git a/nhf/touhou/shiki_eiki/crown.py b/nhf/touhou/shiki_eiki/crown.py new file mode 100644 index 0000000..dff7452 --- /dev/null +++ b/nhf/touhou/shiki_eiki/crown.py @@ -0,0 +1,294 @@ +import math +from dataclasses import dataclass, field +import cadquery as Cq +from nhf import Material, Role +from nhf.build import Model, target, assembly +import nhf.utils + +@dataclass +class Crown(Model): + + facets: int = 5 + # Lower circumference + base_circ: float = 570.0 + # Upper circumference + tilt_circ: float = 670.0 + height: float = 120.0 + + margin: float = 10.0 + + def __post_init__(self): + super().__init__(name="crown") + + assert self.tilt_circ > self.base_circ + + @property + def facet_width_lower(self): + return self.base_circ / self.facets + @property + def facet_width_upper(self): + return self.tilt_circ / self.facets + + @target(name="side") + 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="front") + 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, + 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 + needle = ( + Cq.Sketch() + .segment( + (z, needle_y_mid), + (z, scale_base_y), + ) + .segment( + (z, scale_base_y), + (-z, scale_base_y), + ) + .segment( + (-z, scale_base_y), + (-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), + ]) + .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), + ]) + .assemble() + ) + z2 = z * 2 + needle_inner = ( + Cq.Sketch() + .segment( + (z2, needle_y_mid - z2), + (-z2, needle_y_mid - z2) + ) + .segment( + (z2, needle_y_mid - z2), + (z2, needle_y_mid + z2), + ) + .segment( + (-z2, needle_y_mid - z2), + (-z2, needle_y_mid + z2), + ) + .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), + (z2, needle_y_mid + z2), + ]) + .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), + (-z2, needle_y_mid + z2), + ]) + .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_inner, mode='a') + .clean() + ) + + @target(name="side-guard") + 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() diff --git a/nhf/touhou/shiki_eiki/epaulette.py b/nhf/touhou/shiki_eiki/epaulette.py new file mode 100644 index 0000000..38b9d10 --- /dev/null +++ b/nhf/touhou/shiki_eiki/epaulette.py @@ -0,0 +1,12 @@ +import math +from dataclasses import dataclass, field +import cadquery as Cq +from nhf import Material, Role +from nhf.build import Model, target, assembly +import nhf.utils + +@dataclass +class Epaulette(Model): + + def __post_init__(self): + super().__init__(name="epaulette") diff --git a/nhf/touhou/shiki_eiki/rod.py b/nhf/touhou/shiki_eiki/rod.py index 6869637..3b303ac 100644 --- a/nhf/touhou/shiki_eiki/rod.py +++ b/nhf/touhou/shiki_eiki/rod.py @@ -24,6 +24,7 @@ class Rod(Model): fac_window_footer_top: float = 0.59 def __post_init__(self): + super().__init__(name="rod") self.loc_core = Cq.Location.from2d(self.length - self.length_tip, 0) assert self.length_tip * 2 < self.length #assert self.fac_bar_top + self.fac_window_tsumi_top < 1 diff --git a/nhf/utils.py b/nhf/utils.py index 1b022ad..0c83db7 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -53,6 +53,11 @@ def is2d(self: Cq.Location) -> bool: return z == 0 and rx == 0 and ry == 0 Cq.Location.is2d = is2d +def scale(self: Cq.Location, fac: float) -> bool: + (x, y, z), (rx, ry, rz) = self.toTuple() + return Cq.Location(x*fac, y*fac, z*fac, rx, ry, rz) +Cq.Location.scale = scale + def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]: """ Returns position and angle @@ -91,7 +96,7 @@ Cq.Location.with_angle_2d = with_angle_2d def flip_x(self: Cq.Location) -> Cq.Location: (x, y), a = self.to2d() - return Cq.Location.from2d(-x, y, 90 - a) + return Cq.Location.from2d(-x, y, 180 - a) Cq.Location.flip_x = flip_x def flip_y(self: Cq.Location) -> Cq.Location: (x, y), a = self.to2d()