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 from typing import Optional, Union import math from dataclasses import dataclass, field import cadquery as Cq def has_part(li: Optional[list[str]], name: Union[str, list[str]]) -> bool: """ Check if a part exists in a name list """ if li: if isinstance(name, list): return all(n in li for n in name) else: return name in li else: return True 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, ) BOLT_LONG = FlatHeadBolt( # FIXME: measure mass=0.0, diam_head=12.8, height_head=2.8, diam_thread=6.0, height_thread=50.0, pitch=1.0, ) BOLT_BEARING = FlatHeadBolt( # FIXME: measure mass=0.0, diam_head=12.8, height_head=2.8, diam_thread=4.0, height_thread=30.0, pitch=1.0, ) @dataclass(frozen=True) class FlangeCoupler(Model): diam_thread: float = 8.0 diam_inner: float = 10.0 diam_outer: float = 22.0 height: float = 12.0 height_flange: float = 2.0 diam_thread_flange: float = 4.0 n_hole_flange: int = 4 r_hole_flange: float = 8.0 # FIXME: Measure! def generate(self) -> Cq.Workplane: result = ( Cq.Workplane() .cylinder( radius=self.diam_outer/2, height=self.height_flange, centered=(True, True, False), ) .faces(">Z") .cylinder( radius=self.diam_inner/2, height=self.height - self.height_flange, centered=(True, True, False), ) .faces(">Z") .hole(self.diam_thread) ) holes = ( Cq.Workplane() .sketch() .regularPolygon( self.r_hole_flange, self.n_hole_flange, mode="c", tag="holes", ) .vertices(tag="holes") .circle(self.diam_thread_flange/2) .finalize() .extrude(self.height_flange) ) result -= holes result.tagAbsolute("top", (0, 0, self.height), direction="+Z") result.tagAbsolute("bot", (0, 0, 9), direction="-Z") for i in range(self.n_hole_flange): loc = Cq.Location.rot2d(i * 360 / self.n_hole_flange) * Cq.Location(self.r_hole_flange, 0) result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.height_flange), direction="+Z") result.tagAbsolute(f"holeB{i}", loc, direction="-Z") return result @dataclass(frozen=True) class Motor(Model): """ Drive motor for the main barrel """ mass: float = 589.7 voltage: float = 12.0 # V power: float = 30.0 # watts diam_thread: float = 4.0 diam_shaft: float = 8.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 height_base_shaft: float = 20.0 # FIXME: Measure # Distance between anchor and the body dx_anchor: float = 20.2 # FIXME: Measure 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 @property def dist_mount_rotor(self): """ Distance between mount point and shaft """ return self.height_base_shaft + self.height_ring 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) ) ) base_shaft = Cq.Solid.makeBox( length=self.diam_shaft, width=self.diam_shaft, height=self.height_base_shaft, ).moved(-self.diam_shaft/2, -self.diam_shaft/2, self.height_body) shaft = Cq.Solid.makeCylinder( radius=self.diam_shaft/2, height=self.height_shaft, pnt=(0, 0, self.height_body + self.height_base_shaft) ) z_anchor = self.height_body - self.height_ring anchor = Cq.Solid.makeCylinder( radius=self.diam_thread/2, height=self.height_anchor, pnt=(0, 0, z_anchor) ) result = result + base_shaft + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0) result.tagAbsolute("anchor1", (self.dx_anchor, 0, z_anchor), direction="+Z") result.tagAbsolute("anchor2", (-self.dx_anchor, 0, z_anchor), direction="+Z") result.tagAbsolute("shaft", (0, 0, self.height_body + self.height_base_shaft), direction="+Z") 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_thickness: float = 7.8 angle_joint_flange_radius: float = 23.0 angle_joint_flange_extension: float = 23.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 angle_joint_bind_radius: float = 135.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 # Longitudinal shift barrel_shift: float = -20.0 # Gap between the stator edge and the inner face of the barrel stator_gap: float = 3.0 # Radius from barrel centre to axis rotation_radius: float = 64.0 n_bearing_balls: int = 12 # Thickness of bearing disks bearing_thickness: float = 20.0 bearing_track_radius: float = 97.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 bearing_disk_gap: float = 10.0 bearing_spindle_max_diam: float = 13.0 bearing_gasket_extend: float = 12.0 bearing_disk_thickness: float = 25.4 / 16 # Coupling mechanism onto the chassis stator_coupler_width: float = 14.0 stator_coupler_thickness: float = 30.0 stator_coupler_thickness_inner: float = 10.0 stator_bind_radius: float = 117.0 # Extra bind sites for stator to prevent warping stator_bind_extra: int = 2 rotor_inner_radius: float = 36.0 rotor_bind_bolt_diam: float = BOLT_BEARING.diam_thread rotor_bind_radius: float = 82.0 rotor_bind_extra: int = 1 stator_spacer_outer_diam: float = 15.0 rotor_spacer_outer_diam: float = 12.0 handle_base_height: float = 10.0 handle_thickness: float = 17.0 handle_length: float = 140.0 handle_height: float = 50.0 motor: Motor = Motor() flange_coupler: FlangeCoupler = FlangeCoupler() auxiliary_thickness: float = 25.4 / 8 # Distance between bind point and motor's mount points motor_driver_radius: float = 110.0 motor_seat_depth: float = 95.0 motor_seat_radius: float = 45.0 motor_coupler_flange_thickness: float = 10.0 motor_coupler_flange_radius: float = 8.0 motor_coupler_height: float = 100.0 motor_coupler_conn_dx: float = 30.0 motor_coupler_wall_thickness: float = 5.0 motor_coupler_inner_gap: float = 1.0 turning_bar_width: float = 15.0 electronic_mount_dx: float = 50.0 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_spacer: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.PLASTIC_PLA material_barrel: Material = Material.ACRYLIC_BLACK material_brace: Material = Material.PLASTIC_PLA material_fastener: Material = Material.STEEL_STAINLESS material_auxiliary: Material = Material.WOOD_BIRCH 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.rotation_radius + self.barrel_diam / 2 assert BOLT_COMMON.diam_thread < self.rotor_bind_radius < self.bearing_track_radius assert self.rotor_inner_radius < self.bearing_track_radius < self.angle_joint_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_joint_bind_pos(self) -> Cq.Location: """ Planar position of the joint bind position """ th = math.pi / self.n_side x = self.angle_joint_bind_radius * math.cos(th) y = self.angle_joint_bind_radius * math.sin(th) return Cq.Location.from2d(x, y) @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)) @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="motor-coupler") def motor_coupler(self) -> Cq.Workplane: """ Coupler which connects to each barrel to move them. """ x = self.motor_coupler_conn_dx y0 = self.barrel_diam/2 + self.motor_coupler_wall_thickness y = self.motor_coupler_flange_radius t = self.motor_coupler_flange_thickness flange = ( Cq.Workplane() .sketch() .polygon([ (x, y), (0, y0), (-x, y), (-x, -y), (0, -y0), (x, -y), ]) .reset() .push([ (x, 0), (-x, 0) ]) .circle(y, mode="a") .circle(BOLT_BEARING.diam_thread/2, mode="s") .reset() .circle(self.barrel_diam/2, mode="s") .finalize() .extrude(t) ) body = ( Cq.Workplane() .cylinder( radius=self.barrel_diam/2 + self.motor_coupler_wall_thickness, height=self.motor_coupler_height, centered=(True, True, False) ) .faces(">Z") .hole(self.barrel_diam + self.motor_coupler_inner_gap*2) ) result = body + flange result.tagAbsolute("holeT1", (x, 0, t), direction="+Z") result.tagAbsolute("holeT2", (-x, 0, t), direction="+Z") result.tagAbsolute("holeB1", (x, 0, 0), direction="-Z") result.tagAbsolute("holeB2", (-x, 0, 0), direction="-Z") return result @target(name="motor-driver-disk", kind=TargetKind.DXF) def profile_motor_driver_disk(self) -> Cq.Sketch: """ A drive disk mounts onto the motor, and extends into gun barrels to turn them. """ hole_diam = self.barrel_diam - self.barrel_wall_thickness * 2 coupler_holes = [ Cq.Location.rot2d(i * 360 / self.n_side) * Cq.Location.from2d(self.rotation_radius + sx * self.motor_coupler_conn_dx, 0) for i in range(self.n_side) for sx in (-1, 1) ] return ( Cq.Sketch() .circle(self.motor_driver_radius) # Drill out the centre which will accomodate the motor shaft .circle(self.motor.diam_shaft/2, mode="s") # Drill out couplers .reset() .regularPolygon( self.flange_coupler.r_hole_flange, self.flange_coupler.n_hole_flange, mode="c", tag="hole", ) .vertices(tag="hole") .circle(self.flange_coupler.diam_thread_flange/2, mode="s") .reset() .regularPolygon( self.rotation_radius, self.n_side, angle=180 / self.n_side, mode="c", tag="const", ) .vertices(tag="const") .circle(hole_diam/2, mode="s") .reset() # Create coupler holes .push([ loc.to2d_pos() for loc in coupler_holes ]) .circle(BOLT_BEARING.diam_thread /2, mode="s") ) def motor_driver_disk(self) -> Cq.Workplane: result = ( Cq.Workplane() .placeSketch(self.profile_motor_driver_disk()) .extrude(self.auxiliary_thickness) ) n = self.flange_coupler.n_hole_flange for i in range(n): loc = Cq.Location.rot2d(i * 360 / n) * Cq.Location(self.flange_coupler.r_hole_flange, 0) result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.auxiliary_thickness), direction="+Z") result.tagAbsolute(f"holeB{i}", loc, direction="-Z") loc_z = Cq.Location(0, 0, self.auxiliary_thickness) loc_outer = Cq.Location.from2d(self.rotation_radius + self.motor_coupler_conn_dx, 0) loc_inner = Cq.Location.from2d(self.rotation_radius - self.motor_coupler_conn_dx, 0) for i in range(self.n_side): loc_rot = Cq.Location.rot2d(i * 360 / self.n_side) p_outer, _ = (loc_z * loc_rot * loc_outer).toTuple() p_inner, _ = (loc_z * loc_rot * loc_inner).toTuple() result.tagAbsolute(f"holeCOF{i}", p_outer, direction="+Z") result.tagAbsolute(f"holeCIF{i}", p_inner, direction="+Z") return result @target(name="motor-mount-plate", kind=TargetKind.DXF) def profile_motor_mount_plate(self) -> Cq.Sketch: r = self.motor_seat_radius gap = 10.0 hole_dx = self.motor.dx_anchor return ( Cq.Sketch() .circle(r + gap) .regularPolygon(r, self.n_side, mode="c", tag="corner") .vertices(tag="corner") .circle(BOLT_COMMON.diam_thread/2, mode="s") .reset() .push([ (hole_dx, 0), (-hole_dx, 0), ]) .circle(self.motor.diam_thread/2, mode="s") ) def motor_mount_plate(self) -> Cq.Workplane: result = ( Cq.Workplane() .placeSketch(self.profile_motor_mount_plate()) .extrude(self.auxiliary_thickness) ) result.tagAbsolute("anchor1", (self.motor.dx_anchor, 0, 0), direction="-Z") result.tagAbsolute("anchor2", (-self.motor.dx_anchor, 0, 0), direction="-Z") r = self.motor_seat_radius for i in range(self.n_side): angle = i * 360 / self.n_side x, y = (Cq.Location.rot2d(angle) * Cq.Location.from2d(0, r)).to2d_pos() result.tagAbsolute(f"holeF{i}", (x, y, self.auxiliary_thickness), direction="+Z") result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z") return result @assembly() def assembly_motor(self) -> Cq.Assembly: a = ( Cq.Assembly() .addS( self.motor.generate(), name="motor", role=Role.MOTOR, ) .addS( self.flange_coupler.generate(), name="flange_coupler", role=Role.CONNECTION | Role.STRUCTURE, material=self.material_fastener, ) .addS( self.motor_driver_disk(), name="driver_disk", role=Role.CONNECTION | Role.STRUCTURE, material=self.material_auxiliary, ) .addS( self.motor_mount_plate(), name="mount_plate", role=Role.CONNECTION | Role.STRUCTURE, material=self.material_auxiliary, ) .constrain( "mount_plate?anchor1", "motor?anchor1", "Plane", ) .constrain( "mount_plate?anchor2", "motor?anchor2", "Plane", ) .constrain( "flange_coupler?top", "motor?shaft", "Plane" ) ) for i in range(self.flange_coupler.n_hole_flange): a = a.constrain( f"flange_coupler?holeB{i}", f"driver_disk?holeB{i}", "Plane", ) # Add the motor seats assert self.n_side % 2 == 0 for i in range(self.n_side // 2): name_seat = f"seat{i}" a = ( a.addS( self.motor_seat(), name=name_seat, role=Role.STRUCTURE, material=self.material_brace ) .constrain( f"{name_seat}?holeMF1", f"mount_plate?holeB{i*2}", "Plane" ) .constrain( f"{name_seat}?holeMF2", f"mount_plate?holeB{i*2+1}", "Plane" ) ) for i in range(self.n_side): name_coupler = f"coupler{i}" a = ( a.addS( self.motor_coupler(), name=name_coupler, role=Role.CONNECTION, material=self.material_brace, ) .constrain( f"{name_coupler}?holeB1", f"driver_disk?holeCOF{i}", "Plane", ) .constrain( f"{name_coupler}?holeB2", f"driver_disk?holeCIF{i}", "Plane", ) ) return a.solve() @target(name="stator-coupler") def stator_coupler(self) -> Cq.Workplane: """ Couples the stator to the chassis """ r1 = self.angle_joint_bind_radius r2 = self.stator_bind_radius assert r1 > r2 l = r1 - r2 w = self.stator_coupler_width h = self.stator_coupler_thickness h_step = h - self.stator_coupler_thickness_inner intersector = Cq.Solid.makeBox( length=l + w, width=w, height=h_step, ).moved(0, -w/2, 0) profile = ( Cq.Sketch() .rect(l, w) .push([ (-l/2, 0), (l/2, 0), ]) .circle(w/2, mode="a") .push([ (-l/2, 0), (l/2, 0), ]) .circle(BOLT_COMMON.diam_thread/2, mode="s") ) result = ( Cq.Workplane() .placeSketch(profile) .extrude(h) ) dx = l / 2 result = result - intersector result.tagAbsolute(f"holeOB", (-dx, 0, 0), direction="-Z") result.tagAbsolute(f"holeIB", (+dx, 0, h_step), direction="-Z") result.tagAbsolute(f"holeOF", (-dx, 0, h), direction="+Z") result.tagAbsolute(f"holeIF", (+dx, 0, h), direction="+Z") return result @target(name="bearing-stator", kind=TargetKind.DXF) def profile_bearing_stator(self) -> Cq.Sketch: assert self.stator_bind_radius < self.angle_joint_bind_radius return ( Cq.Sketch() .circle(self.bulk_radius - self.side_thickness - self.stator_gap) #.regularPolygon(self.side_width - self.side_thickness - self.stator_gap, self.n_side*2) .circle(self.bearing_track_radius + self.bearing_gap/2, mode="s") .reset() .regularPolygon( self.stator_bind_radius, self.n_side * (1 + self.stator_bind_extra), 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) ) br = self.stator_bind_radius th1 = math.radians(360 / self.n_side) th2 = math.radians(360 / (self.n_side * (1 + self.stator_bind_extra))) for i in range(self.n_side): angle = (i+0.5) * th1 result.faces(">Z").moveTo( br * math.cos(angle), br * math.sin(angle), ).tagPlane(f"holeF{i}") result.faces(" float: return self.bearing_track_radius - self.bearing_gap/2 @target(name="bearing-rotor", kind=TargetKind.DXF) def profile_bearing_rotor(self) -> Cq.Sketch: bolt_angle = (180 / self.n_side) * 1.5 n_binds = 1 + self.rotor_bind_extra return ( Cq.Sketch() .circle(self.rotor_radius) .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( r=self.rotor_bind_radius, n=self.n_side * n_binds, 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_gasket_extend 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(BOLT_BEARING.diam_thread, 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 @target(name="stator-spacer") def stator_spacer(self) -> Cq.Solid: outer = Cq.Solid.makeCylinder( radius=self.stator_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 @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=BOLT_BEARING.diam_thread/2, height=self.bearing_disk_gap ) return outer - inner @property def bearing_spindle_height(self) -> float: h = self.bearing_disk_gap + 2 * self.bearing_disk_thickness return h * 2 @target(name="bearing-spindle") def bearing_spindle(self) -> Cq.Solid: r1 = self.bearing_gap / 2 r2 = self.bearing_spindle_max_diam h = self.bearing_disk_gap + 2 * self.bearing_disk_thickness cone1 = Cq.Solid.makeCylinder( radius=r1, height=h/2, ) cone2 = Cq.Solid.makeCone( radius1=r1, radius2=r2, height=h/2, ) hole = Cq.Solid.makeCylinder( radius=(BOLT_BEARING.diam_thread + 1)/2, height=h*2 ).moved(0, 0, -h) top = (cone1 + cone2.moved(0, 0, h/2)) - hole return top + top.rotate((0,0,0),(1,0,0),180) def barrel(self) -> Cq.Compound: """ One gun barrel """ outer = Cq.Solid.makeCylinder( radius=self.barrel_diam/2, height=self.barrel_length, ) inner = Cq.Solid.makeCylinder( radius=self.barrel_diam/2-self.barrel_wall_thickness, height=self.barrel_length ) return outer - inner @assembly() def assembly_machine(self) -> Cq.Assembly: """ The assembly with gun barrels """ z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness gasket_h = self.bearing_spindle_height / 2 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_bot", material=self.material_bearing, role=Role.ROTOR, loc=Cq.Location(0, 0, -gasket_h-self.bearing_disk_thickness) ) .addS( self.bearing_gasket(), name="gasket_top", material=self.material_bearing, role=Role.ROTOR, loc=Cq.Location(0, 0, gasket_h) ) ) z = -self.bearing_disk_gap/2 da_bind_stator = 360 / self.n_side da_bind_rotor = 360 / self.n_side da_bind_stator_minor = 360 / self.n_side / (1 + self.stator_bind_extra) da_bind_rotor_minor = 360 / self.n_side / (1 + self.rotor_bind_extra) for i in range(self.n_side): loc_barrel = Cq.Location.rot2d((i+1/2) * 360/self.n_side) * \ Cq.Location(self.rotation_radius, 0, self.barrel_shift-self.barrel_length/2) a = a.addS( self.barrel(), name=f"barrel{i}", material=self.material_barrel, role=Role.DECORATION, loc=loc_barrel, ) for j in range(1 + self.rotor_bind_extra): angle = i * da_bind_rotor + (j+0.5) * da_bind_rotor_minor loc = Cq.Location.rot2d(angle) * Cq.Location(self.rotor_bind_radius, 0, z) a = a.addS( self.rotor_spacer(), name=f"spacer_rotor{i}_{j}", material=self.material_spacer, role=Role.STRUCTURE, loc=loc ) for j in range(1 + self.stator_bind_extra): angle = i * da_bind_stator + (j+0.5) * da_bind_stator_minor loc = Cq.Location.rot2d(angle) * Cq.Location(self.stator_bind_radius, 0, z) a = a.addS( self.stator_spacer(), name=f"spacer_stator{i}_{j}", material=self.material_spacer, role=Role.STRUCTURE, loc=loc ) for i in range(self.n_bearing_balls): ball = self.bearing_spindle() 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_spindle{i}", material=self.material_bearing_ball, role=Role.BEARING, loc=loc, ) return a @target(name="turning-bar") def turning_bar(self) -> Cq.Workplane: """ Converts the longitudinal/axial mount points on angle joints to transverse mount points to make them more suitable for electronics. """ _, dx = self.angle_joint_bind_pos.to2d_pos() t = 8 w = self.turning_bar_width result = ( Cq.Workplane() .box( length=dx*2 + w, width=w, height=t, centered=(True, True, False) ) ) flange = Cq.Solid.makeBox( length=w, width=t, height=w/2, ).moved(-w/2, -t, -w/2) + Cq.Solid.makeCylinder( radius=w/2, height=t, pnt=(0, -t, -w/2), dir=(0, 1, 0), ) remover = Cq.Solid.makeCylinder( radius=BOLT_COMMON.diam_thread/2, height=w, ) removerf = Cq.Solid.makeCylinder( radius=BOLT_COMMON.diam_thread/2, height=w*2, pnt=(0, -w, -w/2), dir=(0, 1, 0), ) dxe = self.electronic_mount_dx result = ( result + flange.moved(dx, w/2, 0) + flange.moved(-dx, w/2, 0) - remover.moved(dxe, 0, 0) - remover.moved(-dxe, 0, 0) - removerf.moved(dx, 0, 0) - removerf.moved(-dx, 0, 0) ) result.tagAbsolute("holeBO1", (dx, w/2, -w/2), direction="+Y") result.tagAbsolute("holeBO2", (-dx, w/2, -w/2), direction="+Y") result.tagAbsolute("holeMO1", (dxe, 0, t)) result.tagAbsolute("holeMO2", (-dxe, 0, t)) return result @target(name="motor-seat") def motor_seat(self) -> Cq.Workplane: """ Create new longitudinal mount points closer to the centre axis, and a ring for mounting lights """ bx, by = self.angle_joint_bind_pos.to2d_pos() gap = 10 t1 = 10 base_w = 17.0 theta = math.pi / self.n_side theta2 = theta * 0.5 track_width = 7.0 r0 = self.bulk_radius r1 = self.rotation_radius + gap r2 = self.rotation_radius - gap profile_arc = ( Cq.Sketch() .circle(r1) .circle(r2, mode="s") .polygon([ (0, 0), (r0 * math.cos(theta), r0 * math.sin(theta)), (r0 * math.cos(theta), -r0 * math.sin(theta)), ], mode="i") ) profile_base = ( profile_arc .reset() .polygon([ (bx - base_w/2, by), (bx + base_w/2, by), (bx + base_w/2, -by), (bx - base_w/2, -by), ]) .reset() .polygon([ (r1 * math.cos(theta), r1 * math.sin(theta)), (r1 * math.cos(theta2), r1 * math.sin(theta2)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), (r0 * math.cos(theta), r0 * math.sin(theta)), ]) .polygon([ (r1 * math.cos(theta), -r1 * math.sin(theta)), (r1 * math.cos(theta2), -r1 * math.sin(theta2)), (r0 * math.cos(theta2), -r0 * math.sin(theta2)), (r0 * math.cos(theta), -r0 * math.sin(theta)), ]) .reset() .push([ (bx, by), (bx, -by), ]) .circle(base_w/2, mode="a") .reset() .push([ (bx, by), (bx, -by), ]) .circle(BOLT_COMMON.diam_thread/2, mode="s") ) base = ( Cq.Workplane() .placeSketch(profile_base) .extrude(t1) ) r3 = self.motor_seat_radius r2_5 = r3 + BOLT_COMMON.diam_thread/2 mount_x = r3 * math.cos(theta) mount_y = r3 * math.sin(theta) front = ( Cq.Workplane() .sketch() .circle(r1) .circle(self.rotation_radius+track_width/2, mode="s") .circle(self.rotation_radius-track_width/2) .circle(r2_5, mode="s") .polygon([ (0, 0), (r0 * math.cos(theta), r0 * math.sin(theta)), (r0 * math.cos(theta), -r0 * math.sin(theta)), ], mode="i") .push([ (mount_x, mount_y), (mount_x, -mount_y), ]) .circle(base_w/2) .circle(BOLT_COMMON.diam_thread/2, mode="s") .finalize() .extrude(t1) ) # Construct the connection between the front and back profile_bridge_outer_base = ( Cq.Sketch() .polygon([ (bx - base_w/2, by - base_w/2), (bx + base_w/2, by - base_w/2), (bx + base_w/2, by - base_w*1.5), (bx - base_w/2, by - base_w*1.5), ]) .wires() .val() .moved(0, 0, t1) ) profile_bridge_outer_top = ( Cq.Sketch() .circle(r1) .circle(self.rotation_radius+track_width/2, mode="s") .polygon([ (0, 0), (r0 * math.cos(theta), r0 * math.sin(theta)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), ], mode="i") .wires() .val() .moved(0, 0, self.motor_seat_depth) ) profile_bridge_inner_base = ( Cq.Sketch() .circle(r1) .circle(r2, mode="s") .polygon([ (0, 0), (r0 * math.cos(theta), r0 * math.sin(theta)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), ], mode="i") .wires() .val() ) profile_bridge_inner_top = ( Cq.Sketch() .circle(r1) .circle(r2, mode="s") .polygon([ (0, 0), (r0 * math.cos(theta), r0 * math.sin(theta)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), ], mode="i") .wires() .val() .moved(0, 0, self.motor_seat_depth - t1) ) bridge_outer = Cq.Solid.makeLoft([profile_bridge_outer_base, profile_bridge_outer_top]) bridge_inner = Cq.Solid.makeLoft([profile_bridge_inner_base, profile_bridge_inner_top]) hole_subtractor = Cq.Solid.makeCylinder( radius=BOLT_COMMON.diam_thread/2, height=t1, pnt=((r1+r2)/2, 0, 0) ) result = ( base + front.translate((0, 0, self.motor_seat_depth - t1)) + bridge_outer + bridge_outer.mirror("XZ") + bridge_inner + bridge_inner.mirror("XZ") - hole_subtractor ) # Mark the mount points result.tagAbsolute("holeBB1", (bx, +by, 0), direction="-Z") result.tagAbsolute("holeBB2", (bx, -by, 0), direction="-Z") result.tagAbsolute("holeMF1", (mount_x, +mount_y, self.motor_seat_depth), direction="+Z") result.tagAbsolute("holeMF2", (mount_x, -mount_y, self.motor_seat_depth), direction="+Z") return result 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.angle_joint_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.angle_joint_bind_radius * math.cos(angle) y = self.angle_joint_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 def angle_joint_flange(self) -> Cq.Workplane: 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.angle_joint_bind_radius h = self.angle_joint_flange_thickness # drill hole cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), ) return flange - cyl @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.angle_joint_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 flange = self.angle_joint_flange() result = result + self.angle_joint_flange() th = math.pi / self.n_side ri = self.angle_joint_bind_radius h = self.angle_joint_gap 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 flange = self.angle_joint_flange() result = result + self.angle_joint_flange() th = math.pi / self.n_side ri = self.angle_joint_bind_radius h = self.angle_joint_gap 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 @assembly() 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( origin=(0, 0, -self.handle_base_height) ) .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, ) ) dx = self.handle_length / 2 - self.handle_thickness / 2 assert self.handle_length < w z = self.handle_height - self.handle_thickness / 2 handle = Cq.Solid.makeCylinder( radius=self.handle_thickness/2, height=dx * 2, pnt=(-dx, 0, z), dir=(1, 0, 0), ) pillar = Cq.Solid.makeCylinder( radius=self.handle_thickness/2, height=z, ) joint = Cq.Solid.makeSphere(radius=self.handle_thickness/2) result = ( base + handle + pillar.moved(dx, 0, 0) + pillar.moved(-dx, 0, 0) + joint.moved(dx, 0, z) + joint.moved(-dx, 0, z) ) t = self.handle_base_height for i, (x, y) in enumerate(self.angle_joint_bolt_position): result.tagAbsolute(f"holeLPO{i}", (+x, y, 0), direction="+Z") result.tagAbsolute(f"holeLSO{i}", (-x, y, 0), direction="+Z") result.tagAbsolute(f"holeLPI{i}", (+x, y, -t), direction="-Z") result.tagAbsolute(f"holeLSI{i}", (-x, y, -t), direction="-Z") result.tagAbsolute(f"holeRPO{i}", (+x, -y, 0), direction="+Z") result.tagAbsolute(f"holeRSO{i}", (-x, -y, 0), direction="+Z") result.tagAbsolute(f"holeRPI{i}", (+x, -y, -t), direction="-Z") result.tagAbsolute(f"holeRSI{i}", (-x, -y, -t), direction="-Z") return result @assembly() def assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly: a = Cq.Assembly() if has_part(parts, "section1"): a = a.add( self.assembly_section1(), name="section1", ) if has_part(parts, "ring1"): a = a.add( self.assembly_ring(self.angle_joint()), name="ring1", ) if has_part(parts, "section2"): a = a.add( self.assembly_section(length=self.side_length2, hasFrontHole=True, hasBackHole=True), name="section2", ) if has_part(parts, "ring2"): a = a.add( self.assembly_ring(self.angle_joint()), name="ring2", ) a = a.addS( self.handle(), name="handle", material=self.material_brace, role=Role.HANDLE, ) # Handle constrain for ih, (x, y) in enumerate(self.angle_joint_bolt_position): a = a.constrain( f"handle?holeLPI{ih}", f"ring2/side0?holeLPO{ih}", "Plane", ) a = a.constrain( f"handle?holeRPI{ih}", f"ring2/side0?holeRPO{ih}", "Plane", ) if has_part(parts, "section3"): a = a.add( self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True), name="section3", ) if has_part(parts, "ring3"): a = a.add( self.assembly_ring(self.angle_joint_chamber_front()), name="ring3", ) if has_part(parts, "chamber"): a = a.add( self.assembly_chamber(), name="chamber", ) if has_part(parts, "ring4"): a = a.add( self.assembly_ring(self.angle_joint_chamber_back()), name="ring4", ) if has_part(parts, "chamber_back"): a = a.addS( self.chamber_back(), name="chamber_back", material=self.material_side, role=Role.STRUCTURE | Role.DECORATION, ) if has_part(parts, "motor"): a = a.add(self.assembly_motor(), name="motor") if has_part(parts, "machine"): a = a.add(self.assembly_machine(), name="machine") if has_part(parts, "turning_bar"): a = a.add(self.turning_bar(), name="turning_bar1") if has_part(parts, ["turning_bar", "ring3"]): a = a.constrain( f"turning_bar1?holeBO2", f"ring3/side0?holeStatorL", "Plane", ) a = a.constrain( f"turning_bar1?holeBO1", f"ring3/side1?holeStatorL", "Plane", ) # FIXME: Filter if has_part(parts, ["motor", "ring2"]): for i in range(self.n_side // 2): j = self.n_side // 2 - 1 - i a = a.constrain( f"motor/seat{j}?holeBB1", f"ring2/side{i*2}?holeStatorL", "Plane", ) #a = a.constrain( # f"motor/seat{j}?holeBB2", # f"ring2/side{i*2+1}?holeStatorL", # "Plane", #) for i in range(self.n_side): j = (i + 1) % self.n_side ir = (self.n_side - i) % self.n_side coupler_name = f"stator_coupler{i}" a = a.addS( self.stator_coupler(), name=coupler_name, material=self.material_brace, role=Role.STRUCTURE, ) a = a.constrain( f"{coupler_name}?holeOB", f"ring1/side{i}?holeStatorL", "Plane", ) a = a.constrain( f"{coupler_name}?holeIF", f"machine/stator2?holeB{ir}", "Plane", ) name_bolt =f"stator_outer_bolt{i}" a = a.addS( BOLT_LONG.generate(), name=name_bolt, material=self.material_fastener, role=Role.CONNECTION, ) a = a.constrain( f"{coupler_name}?holeOF", f"{name_bolt}?root", "Plane", ) name_bolt =f"chamber_back{i}boltFPI{i}" 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", ) 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 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()