from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.materials import Role, Material import nhf.utils import math from dataclasses import dataclass, field import cadquery as Cq @dataclass class Onbashira(Model): n_side: int = 6 # Dimensions of each side panel side_width: float = 150.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 section1_gohei_loc: float = 30.0 gohei_bolt_diam: float = 6.0 # Joints between two sets of side panels angle_joint_thickness: float = 15.0 # Z-axis size of each angle joint 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 = 6.0 angle_joint_bolt_head_diam: float = 13.0 angle_joint_bolt_head_depth: float = 3.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ (40, 12), ]) angle_joint_flange_thickness: float = 7.8 angle_joint_flange_radius: float = 30.0 angle_joint_conn_thickness: float = 6.0 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 # Radius from barrel centre to axis rotation_radius: float = 66.0 n_bearing_balls: int = 12 # Size of ball bearings bearing_ball_diam: float = 25.4 * 1/2 bearing_ball_gap: float = .5 # Thickness of bearing disks bearing_thickness: float = 20.0 bearing_track_radius: float = 100.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 bearing_disk_thickness: float = 25.4 / 8 rotor_inner_radius: float = 40.0 rotor_bind_bolt_diam: float = 6.0 rotor_bind_radius: float = 78.0 rotor_spacer_outer_diam: float = 15.0 stator_bind_radius: float = 130.0 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_spacer: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT material_brace: Material = Material.PLASTIC_PLA def __post_init__(self): assert self.n_side >= 3 # Bulk must be large enough for the barrel + bearing to rotate assert self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam > self.rotation_radius + self.barrel_diam / 2 assert self.bearing_gap < 0.95 * self.bearing_ball_diam assert self.rotor_bind_bolt_diam < self.rotor_bind_radius < self.bearing_track_radius 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 assert self.angle_joint_depth / 2 > self.angle_joint_conn_depth assert self.angle_joint_thickness > self.angle_joint_conn_thickness # Ensure the stator could be printed on a 12x12in board assert self.side_width * 2 < 12 * 25.4 @property 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 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) """ return self.side_width - self.delta_side_width @property def angle_joint_extra_width(self) -> float: theta = math.pi / self.n_side dt = self.angle_joint_thickness * math.tan(theta) return dt * 2 @property def angle_dihedral(self) -> float: return 180 - self.angle_side @property def bulk_radius(self) -> float: """ Radius of the bulk (surface of each side) to the centre """ 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 @property def bearing_disk_gap(self) -> float: """ Gap between two bearing disks to touch the bearing balls """ diag = self.bearing_ball_diam dx = self.bearing_gap return math.sqrt(diag ** 2 - dx ** 2) @target(name="bearing-stator", kind=TargetKind.DXF) def profile_bearing_stator(self) -> Cq.Sketch: return ( Cq.Sketch() .regularPolygon(self.side_width - self.side_thickness, self.n_side) .circle(self.bearing_track_radius + self.bearing_gap/2, mode="s") .reset() .regularPolygon( self.stator_bind_radius, self.n_side, mode="c", tag="bolt") .vertices(tag="bolt") .circle(self.rotor_bind_bolt_diam/2, mode="s") ) def bearing_stator(self) -> Cq.Workplane: result = ( Cq.Workplane() .placeSketch(self.profile_bearing_stator()) .extrude(self.bearing_disk_thickness) ) for i in range(self.n_side): angle = (i+0.5) * math.radians(360 / self.n_side) result.faces(">Z").moveTo( self.stator_bind_radius * math.cos(angle), self.stator_bind_radius * math.sin(angle), ).tagPlane(f"holeF{i}") result.faces(" Cq.Sketch: bolt_angle = 180 / self.n_side return ( Cq.Sketch() .circle(self.bearing_track_radius - self.bearing_gap/2) .circle(self.rotor_inner_radius, mode="s") .reset() .regularPolygon( self.rotation_radius, self.n_side, mode="c", tag="corners") .vertices(tag="corners") .circle(self.barrel_diam/2, mode="s") .reset() .regularPolygon( self.rotor_bind_radius, self.n_side, mode="c", tag="bolt", angle=bolt_angle) .vertices(tag="bolt") .circle(self.rotor_bind_bolt_diam/2, mode="s") ) def bearing_rotor(self) -> Cq.Workplane: return ( Cq.Workplane() .placeSketch(self.profile_bearing_rotor()) .extrude(self.bearing_disk_thickness) ) @target(name="bearing-gasket", kind=TargetKind.DXF) def profile_bearing_gasket(self) -> Cq.Sketch: dr = self.bearing_ball_diam eps = 0.05 return ( Cq.Sketch() .circle(self.bearing_track_radius + dr) .circle(self.bearing_track_radius - dr, mode="s") .reset() .regularPolygon( self.bearing_track_radius, self.n_bearing_balls, mode="c", tag="corners") .vertices(tag="corners") .circle(self.bearing_ball_diam/2 * (1+eps), mode="s") ) def bearing_gasket(self) -> Cq.Workplane: return ( Cq.Workplane() .placeSketch(self.profile_bearing_gasket()) .extrude(self.bearing_disk_thickness) ) @target(name="pipe", kind=TargetKind.DXF) def pipe(self) -> Cq.Sketch: """ The rotating pipes. Purely for decoration """ pass def bearing_ball(self) -> Cq.Solid: return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) @target(name="rotor-spacer") def rotor_spacer(self) -> Cq.Solid: outer = Cq.Solid.makeCylinder( radius=self.rotor_spacer_outer_diam/2, height=self.bearing_disk_gap, ) inner = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=self.bearing_disk_gap ) return outer - inner def assembly_barrel(self) -> Cq.Assembly: z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness a = ( Cq.Assembly() .addS( self.bearing_stator(), name="stator1", material=self.material_bearing, role=Role.STATOR, loc=Cq.Location(0, 0, self.bearing_disk_gap/2) ) .addS( self.bearing_rotor(), name="rotor1", material=self.material_bearing, role=Role.ROTOR, loc=Cq.Location(0, 0, self.bearing_disk_gap/2) ) .addS( self.bearing_stator(), name="stator2", material=self.material_bearing, role=Role.STATOR, loc=Cq.Location(0, 0, z_lower) ) .addS( self.bearing_rotor(), name="rotor2", material=self.material_bearing, role=Role.ROTOR, loc=Cq.Location(0, 0, z_lower) ) .addS( self.bearing_gasket(), name="gasket", material=self.material_bearing, role=Role.ROTOR, loc=Cq.Location(0, 0, -self.bearing_disk_thickness/2) ) ) z = -self.bearing_disk_gap/2 for i in range(self.n_side): loc = Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.rotor_bind_radius, 0, z) a = a.addS( self.rotor_spacer(), name=f"spacerRotor{i}", material=self.material_spacer, role=Role.STRUCTURE, loc=loc ) loc = Cq.Location.rot2d((i+0.5) * 360/self.n_side) * Cq.Location(self.stator_bind_radius, 0, z) a = a.addS( self.rotor_spacer(), name=f"spacerStator{i}", material=self.material_spacer, role=Role.STRUCTURE, loc=loc ) for i in range(self.n_bearing_balls): ball = self.bearing_ball() loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0) a = a.addS( ball, name=f"bearing_ball{i}", material=self.material_bearing_ball, role=Role.BEARING, loc=loc, ) return a 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, 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, length: float, hasFrontHole: bool = True, hasBackHole: bool = True, ) -> Cq.Workplane: w = self.side_width sketch = self.profile_side_panel( length=length, hasFrontHole=hasFrontHole, hasBackHole=hasBackHole, ) 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.bulk_radius), ]) .close() .extrude(length) .translate(Cq.Vector(0, length/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 = 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 @target(name="side-panel1", kind=TargetKind.DXF) def profile_side_panel1(self) -> Cq.Sketch: return ( self.profile_side_panel( length=self.side_length1, hasFrontHole=False, hasBackHole=True, ) .push([ (0, self.side_length1/2 - self.section1_gohei_loc) ]) .circle(self.gohei_bolt_diam/2, mode="s") ) def side_panel1(self) -> Cq.Workplane: l = self.side_length1 w = self.side_width sketch = self.profile_side_panel1() 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.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 @target(name="side-panel2", kind=TargetKind.DXF) def profile_side_panel2(self) -> Cq.Sketch: return ( self.profile_side_panel( length=self.side_length2, hasFrontHole=True, hasBackHole=True, ) ) @target(name="side-panel3", kind=TargetKind.DXF) def profile_side_panel3(self) -> Cq.Sketch: return ( self.profile_side_panel( length=self.side_length3, hasFrontHole=True, hasBackHole=True, ) ) def assembly_section1(self) -> Cq.Assembly: a = Cq.Assembly() side = self.side_panel1() r = self.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 def assembly_section(self, **kwargs) -> Cq.Assembly: a = Cq.Assembly() side = self.side_panel(**kwargs) r = self.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="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") th = math.pi / self.n_side r = self.bulk_radius flange = ( Cq.Workplane() .sketch() .push([ (r, r * math.tan(th)) ]) .circle(self.angle_joint_flange_radius) .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") .finalize() .extrude(self.angle_joint_flange_thickness) .translate((0, 0, -self.angle_joint_flange_thickness/2)) ) ri = self.stator_bind_radius h = self.angle_joint_flange_thickness cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), ) result = result + flange - cyl result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") 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))) .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") th = math.pi / self.n_side r = self.bulk_radius flange_z = self.angle_joint_depth / 2 - self.side_thickness flange = ( Cq.Workplane() .sketch() .push([ (r, r * math.tan(th)) ]) .circle(self.angle_joint_flange_radius) .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") .finalize() .extrude(self.angle_joint_flange_thickness) .translate((0, 0, -flange_z)) ) ri = self.stator_bind_radius h = self.angle_joint_flange_thickness cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(ri * math.cos(th), ri * math.sin(th), -flange_z), ) result = result + flange - cyl result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), -flange_z+h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -flange_z-h/2), direction="-Z") return result @target(name="angle-joint") def angle_joint(self) -> Cq.Workplane: """ 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 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 """ # 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) ) 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 + (self.angle_joint_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 result = ( Cq.Workplane() .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))) .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) .intersect(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.bulk_radius + self.angle_joint_thickness 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"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"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") return result @target(name="angle-joint-flanged") def angle_joint_flanged(self) -> Cq.Workplane: result = self.angle_joint() th = math.pi / self.n_side r = self.bulk_radius flange = ( Cq.Workplane() .sketch() .push([ (r, r * math.tan(th)) ]) .circle(self.angle_joint_flange_radius) .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") .finalize() .extrude(self.angle_joint_flange_thickness) .translate((0, 0, -self.angle_joint_flange_thickness/2)) ) ri = self.stator_bind_radius h = self.angle_joint_flange_thickness cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), ) result = result + flange - cyl result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result def assembly_ring(self, base) -> Cq.Assembly: a = Cq.Assembly() r = self.bulk_radius for i in range(self.n_side): a = a.addS( base, name=f"side{i}", material=self.material_brace, role=Role.CASING | Role.DECORATION, loc=Cq.Location.rot2d(i*360/self.n_side), ) return a @assembly() def assembly(self) -> Cq.Assembly: a = Cq.Assembly() a = ( a .add( self.assembly_section1(), name="section1", ) .add( self.assembly_ring(self.angle_joint_flanged()), name="ring1", ) .add( self.assembly_section(length=self.side_length2, hasFrontHole=True, hasBackHole=True), name="section2", ) .add( self.assembly_ring(self.angle_joint()), name="ring2", ) .add( self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True), name="section3", ) .add( self.assembly_ring(self.angle_joint_chamber_front()), name="chamber_front", ) .add( self.assembly_chamber(), name="chamber", ) .add( self.assembly_ring(self.angle_joint_chamber_back()), name="chamber_back", ) .add(self.assembly_barrel(), name="barrel") ) 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", ) a = a.constrain( f"barrel/stator2?holeB{i}", f"ring1/side{i}?holeStatorR", "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", ) if nr: a = a.constrain( f"{nr}/side{i}?holeFPO{ih}", f"{nc}/side{i}?holeRSM{ih}", "Plane", ) return a.solve()