import math from dataclasses import dataclass, field from typing import Tuple import cadquery as Cq from nhf import Material, Role from nhf.build import Model, target, assembly, TargetKind 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 / 8 # The side which has mounted hinges must be thicker thickness_side: float = 25.4 / 4 height_internal: float = 30.0 material_shell: Material = Material.WOOD_BIRCH # Considering the glyph on the top ... # 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.36 fac_window_footer_top: float = 0.6 # Considering the side ... hinge_plate_pos: list[float] = field(default_factory=lambda: [0.1, 0.9]) hinge_plate_length: float = 30.0 hinge_hole_diam: float = 2.5 # Hole distance to axis hinge_hole_axis_dist: float = 12.5 / 2 # Distance between holes hinge_hole_sep: float = 15.89 # Consider the reference objects ref_object_width: float = 50.0 ref_object_length: float = 50.0 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 profile_points(self) -> list[Tuple[str, Tuple[float, float]]]: """ Points in polygon line order, labaled """ return [ ("tip", (self.length, 0)), ("mid_r", (self.length - self.length_tip, self.width/2)), ("bot_r", (0, self.width_tail / 2)), ("bot_l", (0, -self.width_tail / 2)), ("mid_l", (self.length - self.length_tip, -self.width/2)), ] 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.65 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="bottom", kind=TargetKind.DXF) def profile_bottom(self) -> Cq.Sketch: return ( Cq.Sketch() .polygon([p for _, p in self.profile_points()]) ) @target(name="top", kind=TargetKind.DXF) def profile_top(self) -> Cq.Sketch: return ( self.profile_bottom() .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') ) def surface_top(self) -> Cq.Workplane: return ( Cq.Workplane('XY') .placeSketch(self.profile_top()) .extrude(self.thickness_top) ) def surface_bottom(self) -> Cq.Workplane: surface = ( Cq.Workplane('XY') .placeSketch(self.profile_bottom()) .extrude(self.thickness_top) ) plane = surface.faces(">Z").workplane() for (name, p) in self.profile_points(): plane.moveTo(*p).tagPlane(name) return surface # Properties of the side surfaces @property def length_edge_tip(self): return math.sqrt(self.length_tip ** 2 + (self.width / 2) ** 2) @property def length_edge_tail(self): dw = (self.width - self.width_tail) / 2 return math.sqrt(self.length_tail ** 2 + dw ** 2) @property def tip_incident_angle(self): """ Angle (measuring from vertical) at which the tip edge pieces must be sanded in order to make them not collide into each other. """ return math.atan2(self.length_tip, self.width / 2) @property def shoulder_incident_angle(self) -> float: angle_tip = math.atan2(self.width / 2, self.length_tip) angle_tail = math.atan2((self.width - self.width_tail) / 2, self.length_tail) return (angle_tip + angle_tail) / 2 @target(name="ref-tip") def ref_tip(self) -> Cq.Workplane: angle = self.tip_incident_angle w = self.ref_object_width drop = math.sin(angle) * w profile = ( Cq.Sketch() .polygon([ (0, 0), (0, w), (w, w), (w - drop, 0), ]) ) return ( Cq.Workplane() .placeSketch(profile) .extrude(self.ref_object_length) ) @target(name="ref-shoulder") def ref_shoulder(self) -> Cq.Workplane: angle = self.shoulder_incident_angle w = self.ref_object_width drop = math.sin(angle) * w profile = ( Cq.Sketch() .polygon([ (0, 0), (0, w), (w, w), (w - drop, 0), ]) ) return ( Cq.Workplane() .placeSketch(profile) .extrude(self.ref_object_length) ) @target(name="side-tip-2x", kind=TargetKind.DXF) def profile_side_tip(self): l = self.length_edge_tip w = self.height_internal return ( Cq.Sketch() .push([(l/2, w/2)]) .rect(l, w) ) @target(name="side-tail", kind=TargetKind.DXF) def profile_side_tail(self): """ Plain side 2 with no hinge """ l = self.length_edge_tail w = self.height_internal return ( Cq.Sketch() .push([(l/2, w/2)]) .rect(l, w) ) @target(name="side-hinge-plate", kind=TargetKind.DXF) def profile_side_hinge_plate(self): l = self.hinge_plate_length w = self.height_internal / 2 return ( Cq.Sketch() .push([(0, w/2)]) .rect(l, w) .push([ (self.hinge_hole_sep / 2, self.hinge_hole_axis_dist), (-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist), ]) .circle(self.hinge_hole_diam / 2, mode='s') ) @target(name="side-tail-hinged", kind=TargetKind.DXF) def profile_side_tail_hinged(self): """ Plain side 2 with no hinge """ l = self.length_edge_tail w = self.height_internal # Holes for hinge plate_pos = [ (t * l, w * 3/4) for t in self.hinge_plate_pos ] hole_pos = [ (self.hinge_hole_sep / 2, self.hinge_hole_axis_dist), (-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist), ] return ( self.profile_side_tail() .push(plate_pos) .rect(self.hinge_plate_length, w/2, mode='s') .push([ (hx + px, w/2 - hy) for hx, hy in hole_pos for px, _ in plate_pos ]) .circle(self.hinge_hole_diam / 2, mode='s') ) @target(name="side-bot", kind=TargetKind.DXF) def profile_side_bot(self): l = self.width_tail - self.thickness_side * 2 w = self.height_internal return ( Cq.Sketch() .rect(l, w) ) def surface_side_tip(self): result = ( Cq.Workplane('XY') .placeSketch(self.profile_side_tip()) .extrude(self.thickness_side) ) plane = result.faces(">Y").workplane() plane.moveTo(0, 0).tagPlane("bot") plane.moveTo(-self.length_edge_tip, 0).tagPlane("top") return result def surface_side_tail(self): result = ( Cq.Workplane('XY') .placeSketch(self.profile_side_tail()) .extrude(self.thickness_side) ) plane = result.faces(">Y").workplane() plane.moveTo(0, 0).tagPlane("bot") plane.moveTo(-self.length_edge_tail, 0).tagPlane("top") return result def surface_side_tail_hinged(self): result = ( Cq.Workplane('XY') .placeSketch(self.profile_side_tail_hinged()) .extrude(self.thickness_side) ) plane = result.faces(">Y").workplane() plane.moveTo(0, 0).tagPlane("bot") plane.moveTo(-self.length_edge_tail, 0).tagPlane("top") return result def surface_side_bot(self): result = ( Cq.Workplane('XY') .placeSketch(self.profile_side_bot()) .extrude(self.thickness_side) ) plane = result.faces(">Y").workplane() plane.moveTo(self.width_tail / 2, 0).tagPlane("bot") plane.moveTo(-self.width_tail / 2, 0).tagPlane("top") return result @assembly() def assembly(self) -> Cq.Assembly: a = ( Cq.Assembly() .addS( self.surface_top(), name="top", material=self.material_shell, role=Role.STRUCTURE | Role.DECORATION ) .constrain("top", "Fixed") .addS( self.surface_bottom(), name="bottom", material=self.material_shell, role=Role.STRUCTURE, loc=Cq.Location(0, 0, -self.thickness_top - self.height_internal) ) .constrain("bottom", "Fixed") .addS( self.surface_side_tip(), name="side_tip_l", material=self.material_shell, role=Role.STRUCTURE, ) .constrain("bottom?tip", "side_tip_l?top", "Plane") .constrain("bottom?mid_l", "side_tip_l?bot", "Plane") .addS( self.surface_side_tip(), name="side_tip_r", material=self.material_shell, role=Role.STRUCTURE, ) .constrain("bottom?tip", "side_tip_r?bot", "Plane") .constrain("bottom?mid_r", "side_tip_r?top", "Plane") .addS( self.surface_side_tail(), name="side_tail_l", material=self.material_shell, role=Role.STRUCTURE, ) .constrain("bottom?mid_l", "side_tail_l?top", "Plane") .constrain("bottom?bot_l", "side_tail_l?bot", "Plane") .addS( self.surface_side_tail_hinged(), name="side_tail_r", material=self.material_shell, role=Role.STRUCTURE, ) .constrain("bottom?mid_r", "side_tail_r?bot", "Plane") .constrain("bottom?bot_r", "side_tail_r?top", "Plane") .addS( self.surface_side_bot(), name="side_bot", material=self.material_shell, role=Role.STRUCTURE, ) .constrain("bottom?bot_l", "side_bot?top", "Plane") .constrain("bottom?bot_r", "side_bot?bot", "Plane") .solve() ) return a