From 21b3c98856282403767fb411e436fb94ac92eda2 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 20 Nov 2024 23:21:37 -0800 Subject: [PATCH] feat: Rod assembly --- nhf/touhou/shiki_eiki/rod.py | 302 ++++++++++++++++++++++++++++++++--- 1 file changed, 282 insertions(+), 20 deletions(-) diff --git a/nhf/touhou/shiki_eiki/rod.py b/nhf/touhou/shiki_eiki/rod.py index b93c634..741757b 100644 --- a/nhf/touhou/shiki_eiki/rod.py +++ b/nhf/touhou/shiki_eiki/rod.py @@ -1,5 +1,6 @@ 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 @@ -14,8 +15,15 @@ class Rod(Model): width_tail: float = 60.0 margin: float = 10.0 - thickness_top: float = 25.4 / 4 - thickness_side: float = 25.4 / 8 + 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 @@ -23,10 +31,21 @@ class Rod(Model): 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 + fac_window_footer_bot: float = 0.36 + fac_window_footer_top: float = 0.6 - material_shell: Material = Material.WOOD_BIRCH + # 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") @@ -49,6 +68,18 @@ class Rod(Model): 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 @@ -173,7 +204,7 @@ class Rod(Model): y_skip = dy_eye + dy_border # Construction of the bottom part - x_bot = dx * 0.6 + x_bot = dx * 0.65 y3 = dy * 0.4 y2 = dy * 0.2 y1 = dy * 0.1 @@ -280,21 +311,17 @@ class Rod(Model): .moved(loc) ) - @target(name="surface", kind=TargetKind.DXF) - def profile_top(self) -> Cq.Sketch: - sketch = ( + @target(name="bottom", kind=TargetKind.DXF) + def profile_bottom(self) -> Cq.Sketch: + return ( 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), - ]) + .polygon([p for _, p in self.profile_points()]) ) - sketch = ( - sketch + @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') @@ -302,15 +329,200 @@ class Rod(Model): .boolean(self._window_tsumi(), mode='s') .boolean(self._window_footer(), mode='s') ) - return sketch def surface_top(self) -> Cq.Workplane: return ( - Cq.Workplane('XZ') + 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([(l/2, 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 = ( @@ -321,5 +533,55 @@ class Rod(Model): 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