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 = 200.0 side_length: float = 600.0 side_thickness: float = 25.4 / 8 # Joints between two sets of side panels angle_joint_thickness: float = 25.4 / 4 # 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 = 10.0 angle_joint_bolt_length: float = 50.0 angle_joint_bolt_diam: float = 10.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_hole_position: list[float] = field(default_factory=lambda: [ (20, 20), (70, 20), ]) # Dimensions of gun barrels barrel_diam: float = 25.4 * 2 barrel_length: float = 300.0 # Radius from barrel centre to axis rotation_radius: float = 90.0 n_bearing_balls: int = 24 # 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 = 135.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 = 55.0 rotor_bind_bolt_diam: float = 10.0 rotor_bind_radius: float = 110.0 stator_bind_radius: float = 170.0 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT material_brace: Material = Material.METAL_AL 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 @property def angle_side(self) -> float: return 360 / self.n_side @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) """ theta = math.pi / self.n_side dt = self.side_thickness * math.tan(theta) return self.side_width - dt * 2 @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 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: return ( Cq.Workplane() .placeSketch(self.profile_bearing_stator()) .extrude(self.bearing_disk_thickness) ) @target(name="bearing-rotor", kind=TargetKind.DXF) def profile_bearing_rotor(self) -> 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 @target(name="side-panel", kind=TargetKind.DXF) def profile_side_panel(self) -> Cq.Sketch: return ( Cq.Sketch() .rect(self.side_width, self.side_length) ) def side_panel(self) -> Cq.Workplane: w = self.side_width l = self.side_length result = ( Cq.Workplane() .placeSketch(self.profile_side_panel()) .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 return result * intersector def angle_joint(self) -> Cq.Workplane: """ Angular joint between two side panels. 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. """ # Create the slot carving slot = ( Cq.Sketch() .regularPolygon( self.side_width, self.n_side ) .regularPolygon( self.side_width_inner, self.n_side, mode="s", ) ) slot = ( Cq.Workplane() .placeSketch(slot) .extrude(self.angle_joint_depth) ) # Construct the overall shape of the joint, and divide it into sections for printing later. sketch = ( Cq.Sketch() .regularPolygon( self.side_width + self.angle_joint_extra_width, self.n_side ) .regularPolygon( self.side_width - self.angle_joint_extra_width, self.n_side, mode="s" ) ) 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)) ) result = ( Cq.Workplane() .placeSketch(sketch) .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) ) return result def bearing_ball(self) -> Cq.Solid: return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) def assembly_rotor(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) ) ) 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 assembly(self) -> Cq.Assembly: a = Cq.Assembly() side = self.side_panel() r = self.bulk_radius a = a.add(self.assembly_rotor(), name="rotor") 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