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 Rod(Model): width: float = 120.0 length: float = 550.0 length_tip: float = 100.0 width_tail: float = 60.0 margin: float = 10.0 thickness_top: float = 25.4 / 4 thickness_side: float = 25.4 / 8 # 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 material_shell: Material = Material.WOOD_BIRCH 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 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 profile_top(self) -> Cq.Sketch: sketch = ( Cq.Sketch() .polygon([ (self.length, 0), (self.length - self.length_tip, self.width/2), (0, self.width_tail / 2), (0, -self.width_tail / 2), (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 def surface_top(self) -> Cq.Workplane: return ( Cq.Workplane('XZ') .placeSketch(self.profile_top()) .extrude(self.thickness_top) ) @assembly() def assembly(self) -> Cq.Assembly: a = ( Cq.Assembly() .addS( self.surface_top(), name="top", material=self.material_shell, role=Role.STRUCTURE | Role.DECORATION ) ) return a