from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.materials import Role, Material import nhf.utils from nhf.parts.fasteners import FlatHeadBolt, HexNut, Washer import math from dataclasses import dataclass, field import cadquery as Cq NUT_COMMON = HexNut( # FIXME: measure mass=0.0, diam_thread=6.0, pitch=1.0, thickness=5.0, width=9.89, ) WASHER_COMMON = Washer( # FIXME: measure mass=0.0, diam_thread=6.0, diam_outer=11.68, thickness=1.5, ) BOLT_COMMON = FlatHeadBolt( # FIXME: measure mass=0.0, diam_head=12.8, height_head=2.8, diam_thread=6.0, height_thread=30.0, pitch=1.0, ) @dataclass class Motor(Model): mass: float = 589.7 voltage: float = 12.0 # V power: float = 30.0 # watts diam_thread: float = 4.0 diam_body: float = 51.0 height_body: float = 83.5 diam_ring: float = 25.93 height_ring: float = 6.55 height_shaft: float = 38.1 # Distance between anchor and the body dx_anchor: float = 20.2 height_anchor: float = 10.4 def __post_init__(self): assert self.diam_ring < self.diam_body assert self.height_ring < self.height_body assert self.dx_anchor < self.diam_body / 2 pass def generate(self) -> Cq.Workplane: result = ( Cq.Workplane() .cylinder( radius=self.diam_body/2, height=self.height_body - self.height_ring, centered=(True, True, False) ) .faces(">Z") .cylinder( radius=self.diam_ring/2, height=self.height_ring, centered=(True, True, False) ) ) shaft = Cq.Solid.makeCylinder( radius=self.diam_thread/2, height=self.height_shaft, pnt=(0, 0, self.height_body) ) anchor = Cq.Solid.makeCylinder( radius=self.diam_thread/2, height=self.height_anchor, pnt=(0, 0, self.height_body - self.height_ring) ) result = result + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0) return result @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 # The angle joint bridges between two sets of side panels. # Extra thickness beyond the onbashira's body angle_joint_thickness: float = 10.0 # Z-axis size of each angle joint angle_joint_depth: float = 50.0 # Gap of each angle joint to connect the outside to the inside angle_joint_gap: float = 8.0 angle_joint_bolt_length: float = 50.0 angle_joint_bolt_diam: float = BOLT_COMMON.diam_thread 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, 10), ]) angle_joint_flange_extension: float = 23.0 angle_joint_extra_hole_offset: float = 20.0 # Mating structure on the angle joint angle_joint_conn_thickness: float = 4.0 angle_joint_conn_depth: float = 15.0 angle_joint_conn_width: float = 15.0 chamber_side_length: float = 400.0 chamber_side_width_ex: float = 20.0 # Dimensions of gun barrels barrel_diam: float = 25.4 * 1.5 barrel_wall_thickness: float = 25.4 / 8 barrel_length: float = 25.4 * 12 # 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 = BOLT_COMMON.diam_thread rotor_bind_radius: float = 78.0 rotor_spacer_outer_diam: float = 15.0 stator_bind_radius: float = 135.0 handle_base_height: float = 10.0 handle_thickness: float = 12.0 handle_length: float = 80.0 handle_height: float = 40.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_barrel: Material = Material.ACRYLIC_BLACK material_brace: Material = Material.PLASTIC_PLA material_fastener: Material = Material.STEEL_STAINLESS 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 assert self.barrel_wall_thickness * 2 < self.barrel_diam @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="sanding-block") def sanding_block(self) -> Cq.Workplane: # Dihedral angle / 2 angle = math.radians(180 / self.n_side) r = math.sin(angle) x = 50.0 return ( Cq.Workplane() .sketch() .polygon([(0,0), (0, x), (x, (1-r) * x), (x, 0)]) .finalize() .extrude(self.side_width * 1.5) ) @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: """ The assembly with gun barrels """ 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 @target(name="chamber-back", kind=TargetKind.DXF) def profile_chamber_back(self) -> Cq.Sketch: return ( Cq.Sketch() .regularPolygon(self.side_width - self.side_thickness, self.n_side) .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 chamber_back(self) -> Cq.Workplane: sketch = self.profile_chamber_back() result = ( Cq.Workplane() .placeSketch(sketch) .extrude(self.side_thickness) ) # Mark all attachment points for i in range(self.n_side): angle = (i+0.5) * math.radians(360 / self.n_side) x = self.stator_bind_radius * math.cos(angle) y = self.stator_bind_radius * math.sin(angle) result.tagAbsolute(f"holeF{i}", (x, y, self.side_thickness), direction="+Z") result.tagAbsolute(f"holeB{i}", (x, -y, 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-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 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_extension) .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") .finalize() .extrude(self.angle_joint_gap) .translate((0, 0, -flange_z)) ) ri = self.stator_bind_radius h = self.angle_joint_gap # Drill holes for connectors cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(0, 0, -flange_z), ) result = ( result + flange - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) ) result.tagAbsolute("holeStatorO", (ri * math.cos(th), ri * math.sin(th), -flange_z), direction="-Z") result.tagAbsolute("holeStatorI", (ri * math.cos(th), ri * math.sin(th), -flange_z+h), direction="+Z") return result @target(name="angle-joint-chamber-front") def angle_joint_chamber_front(self) -> Cq.Workplane: """ Angle joint for connecting the chamber to the chassis of the barrel """ # 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") # Generate the flange geometry th = math.pi / self.n_side flange = ( Cq.Workplane() .sketch() .regularPolygon(self.side_width_inner, self.n_side) .regularPolygon(self.side_width_inner - self.angle_joint_flange_extension, self.n_side, mode="s") .finalize() .extrude(self.angle_joint_gap) .translate((0, 0, -self.angle_joint_gap/2)) ) flange = flange * intersector ri = self.stator_bind_radius h = self.angle_joint_gap # Drill holes for connectors cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(0, 0, -h/2), ) side_pos = Cq.Location(ri * math.cos(th), self.angle_joint_extra_hole_offset, 0) side_pos2 = Cq.Location.rot2d(360/self.n_side) * side_pos.flip_y() result = ( result + flange - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) - cyl.moved(side_pos.toTuple()) - cyl.moved(side_pos2.toTuple()) ) 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") 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") # Generate the flange geometry th = math.pi / self.n_side flange = ( Cq.Workplane() .sketch() .regularPolygon(self.side_width_inner, self.n_side) .regularPolygon(self.side_width_inner - self.angle_joint_flange_extension, self.n_side, mode="s") .finalize() .extrude(self.angle_joint_gap) .translate((0, 0, -self.angle_joint_gap/2)) ) flange = flange * intersector ri = self.stator_bind_radius h = self.angle_joint_gap cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(0, 0, -h/2), ) side_pos = Cq.Location(ri * math.cos(th), self.angle_joint_extra_hole_offset, 0) side_pos2 = Cq.Location.rot2d(360/self.n_side) * side_pos.flip_y() result = ( result + flange - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) - cyl.moved(side_pos.toTuple()) - cyl.moved(side_pos2.toTuple()) ) 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 @target(name="handle") def handle(self) -> Cq.Workplane: w = self.side_width + self.angle_joint_extra_width base = ( Cq.Workplane() .box( length=w, width=self.angle_joint_depth, height=self.handle_base_height, centered=(True, True, False) ) .faces(">Z") .workplane() .pushPoints([ (x * sx, y * sy) for (x, y) in self.angle_joint_bolt_position for sx in (-1, 1) for sy in (-1, 1) ]) .cboreHole( self.angle_joint_bolt_diam, self.angle_joint_bolt_head_diam, self.angle_joint_bolt_head_depth, depth=None, ) ) handle = ( Cq.Workplane(origin=(0, 0, self.handle_height)) .box( length=self.handle_length, width=self.handle_thickness, height=self.handle_thickness, ) ) return base + handle @assembly() def assembly(self) -> Cq.Assembly: a = Cq.Assembly() a = ( a .add( self.assembly_section1(), name="section1", ) .add( self.assembly_ring(self.angle_joint()), 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="ring3", ) .add( self.assembly_chamber(), name="chamber", ) .add( self.assembly_ring(self.angle_joint_chamber_back()), name="ring4", ) .addS( self.chamber_back(), name="chamber_back", material=self.material_side, role=Role.STRUCTURE | Role.DECORATION, ) .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"ring3/side{i}?holeRSO{ih}", "Plane", ) a = a.constrain( f"chamber/side{i}?holeBPI{ih}", f"ring4/side{i}?holeLSO{ih}", "Plane", ) a = a.constrain( f"ring4/side{i}?holeStatorO", f"chamber_back?holeB{i}", "Plane", ) a = a.constrain( f"barrel/stator2?holeB{i}", f"ring1/side{i}?holeStatorR", "Plane", ) # Generate bolts for the chamber back name_bolt =f"chamber_back{i}boltFPI{ih}" a = a.addS( BOLT_COMMON.generate(), name=name_bolt, material=self.material_fastener, role=Role.CONNECTION, ) a = a.constrain( f"chamber_back?holeF{i}", f"{name_bolt}?root", "Plane", param=0, ) for (nl, nc, nr) in [ ("section1", "ring1", "section2"), ("section2", "ring2", "section3"), ("section3", "ring3", 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()