Cosplay/nhf/touhou/yasaka_kanako/onbashira.py

352 lines
12 KiB
Python

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