diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 56e368b..b7f8f83 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -17,7 +17,6 @@ class Onbashira(Model): side_length1: float = 200.0 side_length2: float = 350.0 side_length3: float = 400.0 - side_length4: float = 400.0 side_thickness: float = 25.4 / 8 @@ -44,6 +43,9 @@ class Onbashira(Model): angle_joint_conn_depth: float = 20.0 angle_joint_conn_width: float = 20.0 + chamber_side_length: float = 400.0 + chamber_side_width_ex: float = 30.0 + # Dimensions of gun barrels barrel_diam: float = 25.4 * 1.5 barrel_length: float = 300.0 @@ -90,6 +92,14 @@ class Onbashira(Model): def angle_side(self) -> float: return 360 / self.n_side @property + def delta_side_width(self) -> float: + """ + Difference between interior and exterior side width due to side thickness + """ + theta = math.pi / self.n_side + dt = self.side_thickness * math.tan(theta) + return dt * 2 + @property def side_width_inner(self) -> float: """ Interior side width @@ -97,9 +107,7 @@ class Onbashira(Model): If outer width is `wi`, inner width is `wo`, each side's cross section is a trapezoid with sides `wi`, `wo`, and height `h` (side thickness) """ - theta = math.pi / self.n_side - dt = self.side_thickness * math.tan(theta) - return self.side_width - dt * 2 + return self.side_width - self.delta_side_width @property def angle_joint_extra_width(self) -> float: theta = math.pi / self.n_side @@ -117,6 +125,15 @@ class Onbashira(Model): """ return self.side_width / 2 / math.tan(math.radians(self.angle_side / 2)) @property + def chamber_side_width(self) -> float: + return self.side_width + self.chamber_side_width_ex + @property + def chamber_bulk_radius(self) -> float: + """ + Radius of the bulk (surface of each side) to the centre + """ + return self.chamber_side_width / 2 / math.tan(math.radians(self.angle_side / 2)) + @property def bearing_diam(self) -> float: return self.bearing_ball_diam + self.bearing_ball_gap @@ -309,7 +326,12 @@ class Onbashira(Model): .circle(self.angle_joint_bolt_diam/2, mode="s") ) - def side_panel(self, length: float, hasFrontHole: bool = True, hasBackHole: bool = True) -> Cq.Workplane: + def side_panel( + self, + length: float, + hasFrontHole: bool = True, + hasBackHole: bool = True, + ) -> Cq.Workplane: w = self.side_width sketch = self.profile_side_panel( length=length, @@ -450,10 +472,317 @@ class Onbashira(Model): ) return a + @target(name="chamber-side-panel", kind=TargetKind.DXF) + def profile_chamber_side_panel(self) -> Cq.Sketch: + l = self.chamber_side_length + w = self.chamber_side_width + return ( + Cq.Sketch() + .rect(w, l) + .push([ + (sx * x, sy * (l/2 - y)) + for (x, y) in self.angle_joint_bolt_position + for sx in [1, -1] + for sy in [1, -1] + ]) + .circle(self.angle_joint_bolt_diam/2, mode="s") + ) + + def chamber_side_panel(self) -> Cq.Workplane: + w = self.chamber_side_width + l = self.chamber_side_length + sketch = self.profile_chamber_side_panel() + result = ( + Cq.Workplane() + .placeSketch(sketch) + .extrude(self.side_thickness) + ) + # Bevel the edges + intersector = ( + Cq.Workplane('XZ') + .polyline([ + (-w/2, 0), + (w/2, 0), + (0, self.chamber_bulk_radius), + ]) + .close() + .extrude(l) + .translate(Cq.Vector(0, l/2, 0)) + ) + # Intersect the side panel + 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 = l / 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 assembly_chamber(self) -> Cq.Assembly: + a = Cq.Assembly() + side = self.chamber_side_panel() + r = self.chamber_bulk_radius + for i in range(self.n_side): + a = a.addS( + side, + name=f"side{i}", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), + ) + return a + + @target(name="angle-joint-chamber-front") + def angle_joint_chamber_front(self) -> Cq.Workplane: + # This slot cuts the interior of the joint + slot = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.side_width, + self.n_side + ) + .finalize() + .extrude(self.angle_joint_depth) + ) + thickness = self.chamber_bulk_radius - self.bulk_radius + + h = (self.bulk_radius + self.angle_joint_extra_width) * 2 + # Intersector for 1/n of the ring + intersector = ( + Cq.Workplane() + .sketch() + .polygon([ + (0, 0), + (h, 0), + (h, h * math.tan(2 * math.pi / self.n_side)) + ]) + .finalize() + .extrude(self.angle_joint_depth*4) + .translate((0, 0, -self.angle_joint_depth*2)) + ) + # The mating structure + z1 = self.bulk_radius + (thickness - self.angle_joint_conn_thickness) / 2 + z2 = z1 + self.angle_joint_conn_thickness + mating1n = ( + Cq.Workplane() + .sketch() + .polygon([ + (z1, 0), + (z1, self.angle_joint_conn_width), + (z2, self.angle_joint_conn_width), + (z2, 0), + ]) + .finalize() + .extrude(self.angle_joint_conn_depth) + ) + mating1p = mating1n.rotate((0,0,0), (1,0,0), 180) + angle = 360 / self.n_side + + chamber_intersector = ( + Cq.Workplane() + .sketch() + .regularPolygon(self.chamber_side_width, self.n_side) + .regularPolygon(self.chamber_side_width - self.delta_side_width, self.n_side, mode="s") + .finalize() + .extrude(self.angle_joint_depth) + .translate((0,0,-self.angle_joint_depth-self.angle_joint_gap/2)) + ) + result = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.chamber_side_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s" + ) + .finalize() + .extrude(self.angle_joint_depth) + .translate((0, 0, -self.angle_joint_depth/2)) + .cut(slot.translate((0, 0, self.angle_joint_gap/2))) + .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) + .intersect(intersector) + .cut(chamber_intersector) + .cut(mating1n) + .union(mating1p) + .union(mating1n.rotate((0,0,0),(0,0,1),angle)) + .cut(mating1p.rotate((0,0,0),(0,0,1),angle)) + ) + h = self.chamber_bulk_radius + hole_negative = Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_diam/2, + height=h, + pnt=(0,0,0), + dir=(1,0,0), + ) + Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_head_diam/2, + height=self.angle_joint_bolt_head_depth, + pnt=(h,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 + 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"holeLPM{i}", (dr0, x, py), direction="-X") + result.tagAbsolute(f"holeRPM{i}", (dr0, x, -py), direction="-X") + result.tagAbsolute(f"holeLSO{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") + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + dr = self.chamber_bulk_radius - self.side_thickness + dy = self.angle_joint_gap / 2 + 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"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") + return result + + @target(name="angle-joint-chamber-back") + def angle_joint_chamber_back(self) -> Cq.Workplane: + slot = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.side_width, + self.n_side + ) + .finalize() + .extrude(self.angle_joint_depth) + ) + thickness = self.chamber_bulk_radius - self.bulk_radius + + h = (self.bulk_radius + self.angle_joint_extra_width) * 2 + # Intersector for 1/n of the ring + intersector = ( + Cq.Workplane() + .sketch() + .polygon([ + (0, 0), + (h, 0), + (h, h * math.tan(2 * math.pi / self.n_side)) + ]) + .finalize() + .extrude(self.angle_joint_depth*4) + .translate((0, 0, -self.angle_joint_depth*2)) + ) + # The mating structure + z1 = self.bulk_radius + (thickness - self.angle_joint_conn_thickness) / 2 + z2 = z1 + self.angle_joint_conn_thickness + mating1n = ( + Cq.Workplane() + .sketch() + .polygon([ + (z1, 0), + (z1, self.angle_joint_conn_width), + (z2, self.angle_joint_conn_width), + (z2, 0), + ]) + .finalize() + .extrude(self.angle_joint_conn_depth) + ) + mating1p = mating1n.rotate((0,0,0), (1,0,0), 180) + angle = 360 / self.n_side + + chamber_intersector = ( + Cq.Workplane() + .sketch() + .regularPolygon(self.chamber_side_width, self.n_side) + .regularPolygon(self.chamber_side_width - self.delta_side_width, self.n_side, mode="s") + .finalize() + .extrude(self.angle_joint_depth) + .translate((0,0,self.angle_joint_gap/2)) + ) + result = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.chamber_side_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s" + ) + .finalize() + .extrude(self.angle_joint_depth) + .translate((0, 0, -self.angle_joint_depth/2)) + .cut(slot.translate((0, 0, self.angle_joint_gap/2))) + .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) + .intersect(intersector) + .cut(chamber_intersector) + .cut(mating1n) + .union(mating1p) + .union(mating1n.rotate((0,0,0),(0,0,1),angle)) + .cut(mating1p.rotate((0,0,0),(0,0,1),angle)) + ) + h = self.chamber_bulk_radius + hole_negative = Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_diam/2, + height=h, + pnt=(0,0,0), + dir=(1,0,0), + ) + Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_head_diam/2, + height=self.angle_joint_bolt_head_depth, + pnt=(h,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)) + p1r = locrot * Cq.Location((0, -x, dy+y)) + result = result \ + - hole_negative.moved(p1) \ + - hole_negative.moved(p1r) + # Mark the absolute locations of the mount points + dr = self.chamber_bulk_radius - self.side_thickness + dr0 = self.bulk_radius + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + dr = self.chamber_bulk_radius - self.side_thickness + dy = self.angle_joint_gap / 2 + 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"holeLPO{i}", (dr, x, py), direction="+X") + #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + return result + @target(name="angle-joint") def angle_joint(self) -> Cq.Workplane: """ - Angular joint between two side panels. This sits at the intersection of + Angular joint between two side panels (excluding chamber). This sits at the intersection of 4 side panels to provide compressive, shear, and tensile strength. To provide tensile strength along the Z-axis, the panels must be bolted @@ -464,37 +793,18 @@ class Onbashira(Model): (primary/secondary) being joined. O/I corresponds to the outside/inside """ - # Create the slot carving + # This slot cuts the interior of the joint slot = ( - Cq.Sketch() + Cq.Workplane() + .sketch() .regularPolygon( self.side_width, self.n_side ) - #.regularPolygon( - # self.side_width_inner, - # self.n_side, mode="s", - #) - ) - slot = ( - Cq.Workplane() - .placeSketch(slot) + .finalize() .extrude(self.angle_joint_depth) ) - # Construct the overall shape of the joint, and divide it into sections for printing later. - sketch = ( - Cq.Sketch() - .regularPolygon( - self.side_width + self.angle_joint_extra_width, - self.n_side - ) - .regularPolygon( - self.side_width_inner, - self.n_side, mode="s" - ) - ) - h = (self.bulk_radius + self.angle_joint_extra_width) * 2 # Intersector for 1/n of the ring intersector = ( @@ -528,7 +838,16 @@ class Onbashira(Model): angle = 360 / self.n_side result = ( Cq.Workplane() - .placeSketch(sketch) + .sketch() + .regularPolygon( + self.side_width + self.angle_joint_extra_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s" + ) + .finalize() .extrude(self.angle_joint_depth) .translate((0, 0, -self.angle_joint_depth/2)) .cut(slot.translate((0, 0, self.angle_joint_gap/2))) @@ -610,13 +929,12 @@ class Onbashira(Model): result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result - def assembly_ring(self, flanged=False) -> Cq.Assembly: + def assembly_ring(self, base) -> Cq.Assembly: a = Cq.Assembly() - side = self.angle_joint_flanged() if flanged else self.angle_joint() r = self.bulk_radius for i in range(self.n_side): a = a.addS( - side, + base, name=f"side{i}", material=self.material_brace, role=Role.CASING | Role.DECORATION, @@ -634,7 +952,7 @@ class Onbashira(Model): name="section1", ) .add( - self.assembly_ring(flanged=True), + self.assembly_ring(self.angle_joint_flanged()), name="ring1", ) .add( @@ -642,7 +960,7 @@ class Onbashira(Model): name="section2", ) .add( - self.assembly_ring(), + self.assembly_ring(self.angle_joint()), name="ring2", ) .add( @@ -650,32 +968,48 @@ class Onbashira(Model): name="section3", ) .add( - self.assembly_ring(), - name="ring3", + self.assembly_ring(self.angle_joint_chamber_front()), + name="chamber_front", ) .add( - self.assembly_section(length=self.side_length4, hasFrontHole=True, hasBackHole=False), - name="section4", + self.assembly_chamber(), + name="chamber", + ) + .add( + self.assembly_ring(self.angle_joint_chamber_back()), + name="chamber_back", ) ) - for (nl, nc, nr) in [ - ("section1", "ring1", "section2"), - ("section2", "ring2", "section3"), - ("section3", "ring3", "section4"), - ]: - for i in range(self.n_side): - j = (i + 1) % self.n_side - for ih in range(len(self.angle_joint_bolt_position)): + 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"chamber/side{i}?holeFPI{ih}", + f"chamber_front/side{i}?holeRSO{ih}", + "Plane", + ) + a = a.constrain( + f"chamber/side{i}?holeBPI{ih}", + f"chamber_back/side{i}?holeLSO{ih}", + "Plane", + ) + for (nl, nc, nr) in [ + ("section1", "ring1", "section2"), + ("section2", "ring2", "section3"), + ("section3", "chamber_front", None), + ]: 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", - ) + if nr: + 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()