diff --git a/nhf/touhou/shiki_eiki/rod.py b/nhf/touhou/shiki_eiki/rod.py index fc2c541..6869637 100644 --- a/nhf/touhou/shiki_eiki/rod.py +++ b/nhf/touhou/shiki_eiki/rod.py @@ -1,3 +1,4 @@ +import math from dataclasses import dataclass, field import cadquery as Cq from nhf import Material, Role @@ -11,6 +12,270 @@ class Rod(Model): length: float = 550.0 length_tip: float = 100.0 width_tail: float = 60.0 + margin: float = 10.0 + + # counted from middle to the bottom + fac_bar_top: float = 0.1 + # counted from bottom to top + fac_window_tsumi_bot: float = 0.63 + fac_window_tsumi_top: float = 0.88 + + fac_window_footer_bot: float = 0.33 + fac_window_footer_top: float = 0.59 + + def __post_init__(self): + 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 + assert self.fac_window_tsumi_bot < self.fac_window_tsumi_top + + @property + def length_tail(self): + return self.length - self.length_tip + + @property + def _reduced_tip_x(self): + return self.length_tip - self.margin + @property + def _reduced_y(self): + return self.width / 2 - self.margin + @property + def _reduced_tail_y(self): + return self.width_tail / 2 - self.margin + + def _window_tip(self) -> Cq.Sketch: + dxh = self._reduced_tip_x + dy = self._reduced_y + return ( + Cq.Sketch() + .segment( + (dxh, 0), + (dxh / 2, dy / 2), + ) + .bezier([ + (dxh / 2, dy / 2), + (dxh * 0.6, dy * 0.4), + (dxh * 0.6, -dy * 0.4), + (dxh / 2, -dy / 2), + ]) + .segment( + (dxh, 0), + ) + .assemble() + .moved(self.loc_core.to2d_pos()) + ) + def _window_eye(self, refl: bool = False) -> Cq.Sketch: + + sign = -1 if refl else 1 + dxh = self._reduced_tip_x + xm = dxh * 0.45 + dy = sign * self._reduced_y + fac = 0.05 + + p1 = Cq.Location.from2d(xm, sign * self.margin / 2) + p2 = Cq.Location.from2d(dxh * 0.1, sign * self.margin / 2) + p3 = Cq.Location.from2d(dxh * 0.15, dy * 0.55) + p4 = Cq.Location.from2d(dxh * 0.4, dy * 0.45) + d4 = Cq.Location.from2d(dxh * fac, -dy * fac) + + return ( + Cq.Sketch() + .segment( + p1.to2d_pos(), + p2.to2d_pos(), + ) + .bezier([ + p2.to2d_pos(), + (p2 * Cq.Location.from2d(0, dy * fac)).to2d_pos(), + (p3 * Cq.Location.from2d(-dxh * fac, -dy * fac)).to2d_pos(), + p3.to2d_pos(), + ]) + .bezier([ + p3.to2d_pos(), + (p3 * Cq.Location.from2d(0, dy * fac)).to2d_pos(), + (p4 * d4.inverse).to2d_pos(), + p4.to2d_pos(), + ]) + .bezier([ + p4.to2d_pos(), + (p4 * d4).to2d_pos(), + (p1 * Cq.Location.from2d(0, dy * fac)).to2d_pos(), + p1.to2d_pos(), + ]) + .assemble() + .moved(self.loc_core.to2d_pos()) + ) + + def _window_bar(self) -> Cq.Sketch(): + dxh = self._reduced_tip_x + dy = self._reduced_y + dyt = self._reduced_tail_y + dxt = self.length_tail + + ext_fac = self.fac_bar_top + + p_corner = Cq.Location.from2d(0, dy) + p_top = Cq.Location.from2d(0.3 * dxh, 0.7 * dy) + p_bot = Cq.Location.from2d(-ext_fac * dxt, dy + ext_fac * (dyt - dy)) + p_top_int = p_corner * Cq.Location.from2d(.05 * dxh, -.2 * dy) + p_top_ctrl = Cq.Location.from2d(0, .3 * dy) + p_bot_int = p_corner * Cq.Location.from2d(-.15 * dxh, -.2 * dy) + p_bot_ctrl = Cq.Location.from2d(-.25 * dxh, .3 * dy) + + return ( + Cq.Sketch() + .segment( + p_corner.to2d_pos(), + p_top.to2d_pos(), + ) + .segment(p_top_int.to2d_pos()) + .bezier([ + p_top_int.to2d_pos(), + p_top_ctrl.to2d_pos(), + p_top_ctrl.flip_y().to2d_pos(), + p_top_int.flip_y().to2d_pos(), + ]) + .segment(p_top.flip_y().to2d_pos()) + .segment(p_corner.flip_y().to2d_pos()) + .segment(p_bot.flip_y().to2d_pos()) + .segment(p_bot_int.flip_y().to2d_pos()) + .bezier([ + p_bot_int.flip_y().to2d_pos(), + p_bot_ctrl.flip_y().to2d_pos(), + p_bot_ctrl.to2d_pos(), + p_bot_int.to2d_pos(), + ]) + .segment(p_bot.to2d_pos()) + .segment(p_corner.to2d_pos()) + .assemble() + .moved(self.loc_core.to2d_pos()) + ) + + def _window_tsumi(self) -> Cq.Sketch: + dx = (self.fac_window_tsumi_top - self.fac_window_tsumi_bot) * self.length_tail + dy = 2 * self._reduced_y * 0.8 + loc = Cq.Location(self.fac_window_tsumi_bot * self.length_tail, 0) + + # Construction of the top part of the kanji + + dx_top = dx * 0.3 + x_top = dx - dx_top / 2 + dy_top = dy + dy_eye = dy * 0.2 + dy_border = (dy_top - 3 * dy_eye) / 4 + # The skip must follow 3 * eye + 4 * border = dy_top + y_skip = dy_eye + dy_border + + # Construction of the bottom part + x_bot = dx * 0.6 + y3 = dy * 0.4 + y2 = dy * 0.2 + y1 = dy * 0.1 + # x/y-centers of the legs + x_leg0 = x_bot / 14 + dx_leg = x_bot / 7 + y_leg = (y3 + y1) / 2 + + return ( + Cq.Sketch() + .push([(x_top, 0)]) + .rect(dx_top, dy_top) + .push([ + (x_top, -y_skip), + (x_top, 0), + (x_top, y_skip), + ]) + .rect(dx_top / 3, dy_eye, mode='s') + + # Construct the two sides + .push([ + (x_bot / 2, (y2 + y1) / 2), + (x_bot / 2, -(y2 + y1) / 2), + ]) + .rect(x_bot, y2 - y1, mode='a') + .push([ + (x_leg0 + dx_leg, y_leg), + (x_leg0 + 3 * dx_leg, y_leg), + (x_leg0 + 5 * dx_leg, y_leg), + (x_leg0 + dx_leg, -y_leg), + (x_leg0 + 3 * dx_leg, -y_leg), + (x_leg0 + 5 * dx_leg, -y_leg), + ]) + .rect(dx_leg, y3 - y1, mode='a') + + .moved(loc) + ) + + def _window_footer(self) -> Cq.Sketch: + x_bot = self.fac_window_footer_bot * self.length_tail + dx = (self.fac_window_footer_top - self.fac_window_footer_bot) * self.length_tail + loc = Cq.Location(x_bot, 0) + + dy = self._reduced_y * 0.8 + + # eyes + eye_y2 = dy * .5 + eye_y1 = dy * .2 + eye_width = eye_y2 - eye_y1 + eye_x = dx - eye_width / 2 + + # bar polygon + bar_x0 = dx * 0.65 + bar_dx = dx * 0.1 + bar_x1 = bar_x0 + bar_dx + bar_x2 = bar_x0 + bar_dx * 2 + bar_x3 = bar_x0 + bar_dx * 3 + bar_y1 = dy * .75 + assert bar_y1 > eye_y2 + bar_y2 = dy * .9 + assert bar_y1 < bar_y2 + + # Construction of the cross + cross_dx = dx * 0.7 / math.sqrt(2) + cross_dy = dy * 0.2 + + cross = ( + Cq.Sketch() + .rect(cross_dx, cross_dy) + .rect(cross_dy, cross_dx, mode='a') + .moved(Cq.Location.from2d(dx * 0.5, 0, 45)) + ) + return ( + Cq.Sketch() + # eyes + .push([ + (eye_x, (eye_y1 + eye_y2)/2), + (eye_x, -(eye_y1 + eye_y2)/2), + ]) + .rect(eye_width, eye_width, mode='a') + # middle bar + .push([(0,0)]) + .polygon([ + (bar_x1, bar_y1), + (bar_x0, bar_y1), + (bar_x0, bar_y2), + (bar_x3, bar_y2), + (bar_x3, bar_y1), + (bar_x2, bar_y1), + + (bar_x2, -bar_y1), + (bar_x3, -bar_y1), + (bar_x3, -bar_y2), + (bar_x0, -bar_y2), + (bar_x0, -bar_y1), + (bar_x1, -bar_y1), + ], mode='a') + # cross + .boolean(cross, mode='a') + + #.push([(0,0)]) + #.rect(10, 10) + + .moved(loc) + ) + + + @target(name="surface") def top_profile(self) -> Cq.Sketch: @@ -24,6 +289,16 @@ class Rod(Model): (self.length - self.length_tip, -self.width/2), ]) ) + + sketch = ( + sketch + .boolean(self._window_tip(), mode='s') + .boolean(self._window_eye(True), mode='s') + .boolean(self._window_eye(False), mode='s') + .boolean(self._window_bar(), mode='s') + .boolean(self._window_tsumi(), mode='s') + .boolean(self._window_footer(), mode='s') + ) return sketch @assembly() diff --git a/nhf/utils.py b/nhf/utils.py index 13469fd..1b022ad 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -1,13 +1,11 @@ """ Utility functions for cadquery objects """ -import functools -import math -from typing import Optional +import functools, math +from typing import Optional, Union, Tuple, cast import cadquery as Cq from cadquery.occ_impl.solver import ConstraintSpec from nhf import Role -from typing import Union, Tuple, cast from nhf.materials import KEY_ITEM, KEY_MATERIAL # Bug fixes @@ -100,10 +98,17 @@ def flip_y(self: Cq.Location) -> Cq.Location: return Cq.Location.from2d(x, -y, -a) Cq.Location.flip_y = flip_y -def boolean(self: Cq.Sketch, obj, **kwargs) -> Cq.Sketch: +def boolean( + self: Cq.Sketch, + obj: Union[Cq.Face, Cq.Sketch, Cq.Compound], + **kwargs) -> Cq.Sketch: + """ + Performs Boolean operation between a sketch and a sketch-like object + """ return ( self .reset() + # Has to be 0, 0. Translation doesn't work. .push([(0, 0)]) .each(lambda _: obj, **kwargs) )