diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 9263e4c..76610ca 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -12,21 +12,26 @@ class Onbashira(Model): n_side: int = 6 # Dimensions of each side panel side_width: float = 200.0 - side_length: float = 600.0 + + # Side panels have different lengths + side_length1: float = 200.0 + side_length2: float = 350.0 + side_length3: float = 400.0 + side_thickness: float = 25.4 / 8 # Joints between two sets of side panels - angle_joint_thickness: float = 25.4 / 4 + angle_joint_thickness: float = 10.0 # Z-axis size of each angle joint - angle_joint_depth: float = 50.0 + angle_joint_depth: float = 60.0 # Gap of each angle joint to connect the outside to the inside angle_joint_gap: float = 10.0 angle_joint_bolt_length: float = 50.0 angle_joint_bolt_diam: float = 10.0 # Position of the holes, with (0, 0) being the centre of each side - angle_joint_hole_position: list[float] = field(default_factory=lambda: [ - (20, 20), - (70, 20), + angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ + (20, 15), + (70, 15), ]) # Dimensions of gun barrels @@ -54,7 +59,7 @@ class Onbashira(Model): material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT - material_brace: Material = Material.METAL_AL + material_brace: Material = Material.PLASTIC_PLA def __post_init__(self): assert self.n_side >= 3 @@ -65,6 +70,9 @@ class Onbashira(Model): assert self.rotor_inner_radius < self.bearing_track_radius < self.stator_bind_radius assert self.angle_joint_thickness > self.side_thickness + for (x, y) in self.angle_joint_bolt_position: + assert y < self.angle_joint_depth / 2 + @property def angle_side(self) -> float: return 360 / self.n_side @@ -183,19 +191,35 @@ class Onbashira(Model): """ pass - @target(name="side-panel", kind=TargetKind.DXF) - def profile_side_panel(self) -> Cq.Sketch: + def profile_side_panel( + self, + length: float, + hasFrontHole: bool = False, + hasBackHole: bool = True) -> Cq.Sketch: + assert hasFrontHole or hasBackHole + signs = ([1] if hasFrontHole else []) + ([-1] if hasBackHole else []) return ( Cq.Sketch() - .rect(self.side_width, self.side_length) + .rect(self.side_width, length) + .push([ + (sx * x, sy * (length/2 - y)) + for (x, y) in self.angle_joint_bolt_position + for sx in [1, -1] + for sy in signs + ]) + .circle(self.angle_joint_bolt_diam/2, mode="s") ) - def side_panel(self) -> Cq.Workplane: + def side_panel(self, length: float, hasFrontHole: bool = True, hasBackHole: bool = True) -> Cq.Workplane: w = self.side_width - l = self.side_length + sketch = self.profile_side_panel( + length=length, + hasFrontHole=hasFrontHole, + hasBackHole=hasBackHole, + ) result = ( Cq.Workplane() - .placeSketch(self.profile_side_panel()) + .placeSketch(sketch) .extrude(self.side_thickness) ) # Bevel the edges @@ -207,11 +231,27 @@ class Onbashira(Model): (0, self.bulk_radius), ]) .close() - .extrude(l) - .translate(Cq.Vector(0, l/2,0)) + .extrude(length) + .translate(Cq.Vector(0, length/2, 0)) ) # Intersect the side panel - return result * intersector + result = result * intersector + + # Mark all attachment points + t = self.side_thickness + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + px = x + py = length / 2 - y + result.tagAbsolute(f"holeFPI{i}", (+px, py, t), direction="+Z") + result.tagAbsolute(f"holeFSI{i}", (-px, py, t), direction="+Z") + result.tagAbsolute(f"holeFPO{i}", (+px, py, 0), direction="-Z") + result.tagAbsolute(f"holeFSO{i}", (-px, py, 0), direction="-Z") + result.tagAbsolute(f"holeBPI{i}", (+px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBSI{i}", (-px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBPO{i}", (+px, -py, 0), direction="-Z") + result.tagAbsolute(f"holeBSO{i}", (-px, -py, 0), direction="-Z") + + return result def angle_joint(self) -> Cq.Workplane: """ @@ -220,6 +260,10 @@ class Onbashira(Model): To provide tensile strength along the Z-axis, the panels must be bolted onto the angle joint. + + The holes are marked hole(L/R)(P/S)(O/I)(i), where L/R corresponds to the two + sections being joined, and P/S corresponds to the two facets + (primary/secondary) being joined. O/I corresponds to the outside/inside """ # Create the slot carving @@ -276,10 +320,45 @@ class Onbashira(Model): .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) .intersect(intersector) ) + hole_negative = Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_diam/2, + height=h, + pnt=(0,0,0), + dir=(1,0,0), + ) + dy = self.angle_joint_gap / 2 + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + for (x, y) in self.angle_joint_bolt_position: + p1 = Cq.Location((0, x, dy+y)) + p2 = Cq.Location((0, x, -dy-y)) + p1r = locrot * Cq.Location((0, -x, dy+y)) + p2r = locrot * Cq.Location((0, -x, -dy-y)) + result = result \ + - hole_negative.moved(p1) \ + - hole_negative.moved(p2) \ + - hole_negative.moved(p1r) \ + - hole_negative.moved(p2r) + # Mark the absolute locations of the mount points + dr = self.bulk_radius + self.angle_joint_thickness + dr0 = self.bulk_radius + dri = self.bulk_radius - self.angle_joint_thickness + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + py = dy + y + result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + result.tagAbsolute(f"holeRPO{i}", (dr, x, -py), direction="+X") + result.tagAbsolute(f"holeLPM{i}", (dr0, x, py), direction="-X") + result.tagAbsolute(f"holeRPM{i}", (dr0, x, -py), direction="-X") + result.tagAbsolute(f"holeLPI{i}", (dri, x, py), direction="-X") + result.tagAbsolute(f"holeRPI{i}", (dri, x, -py), direction="-X") + result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") + result.tagAbsolute(f"holeLSM{i}", locrot * Cq.Location(dr0, -x, py), direction="-X") + result.tagAbsolute(f"holeRSM{i}", locrot * Cq.Location(dr0, -x, -py), direction="-X") + result.tagAbsolute(f"holeLSI{i}", locrot * Cq.Location(dri, -x, py), direction="-X") + result.tagAbsolute(f"holeRSI{i}", locrot * Cq.Location(dri, -x, -py), direction="-X") return result - def bearing_ball(self) -> Cq.Solid: return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) @@ -335,11 +414,10 @@ class Onbashira(Model): ) return a - def assembly(self) -> Cq.Assembly: + def assembly_section(self, **kwargs) -> Cq.Assembly: a = Cq.Assembly() - side = self.side_panel() + side = self.side_panel(**kwargs) r = self.bulk_radius - a = a.add(self.assembly_rotor(), name="rotor") for i in range(self.n_side): a = a.addS( side, @@ -349,3 +427,62 @@ class Onbashira(Model): loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), ) return a + def assembly_ring(self) -> Cq.Assembly: + a = Cq.Assembly() + side = self.angle_joint() + r = self.bulk_radius + for i in range(self.n_side): + a = a.addS( + side, + name=f"side{i}", + material=self.material_brace, + role=Role.CASING | Role.DECORATION, + loc=Cq.Location.rot2d(i*360/self.n_side), + ) + return a + + def assembly(self) -> Cq.Assembly: + a = Cq.Assembly() + a = ( + a + .add( + self.assembly_section(length=self.side_length1, hasFrontHole=False, hasBackHole=True), + name="section1", + ) + .add( + self.assembly_ring(), + name="ring1", + ) + .add( + self.assembly_section(length=self.side_length2, hasFrontHole=True, hasBackHole=True), + name="section2", + ) + .add( + self.assembly_ring(), + name="ring2", + ) + .add( + self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True), + name="section3", + ) + ) + for (nl, nc, nr) in [ + ("section1", "ring1", "section2"), + ("section2", "ring2", "section3"), + ]: + for i in range(self.n_side): + j = (i + 1) % self.n_side + for ih in range(len(self.angle_joint_bolt_position)): + a = a.constrain( + f"{nl}/side{i}?holeBSO{ih}", + f"{nc}/side{i}?holeLPM{ih}", + "Plane", + ) + a = a.constrain( + f"{nr}/side{i}?holeFPO{ih}", + f"{nc}/side{i}?holeRSM{ih}", + "Plane", + ) + + a = a.add(self.assembly_rotor(), name="rotor") + return a.solve() diff --git a/nhf/utils.py b/nhf/utils.py index 0c83db7..1234796 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -162,6 +162,15 @@ def tagPlane(self, tag: str, Cq.Workplane.tagPlane = tagPlane +def tag_absolute( + self, + tag: str, + loc: Union[Cq.Location, Tuple[float, float, float]], + direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'): + return self.pushPoints([loc]).tagPlane(tag, direction=direction) + +Cq.Workplane.tagAbsolute = tag_absolute + def make_sphere(r: float = 2) -> Cq.Solid: """ Makes a full sphere. The default function makes a hemisphere