Cosplay/nhf/touhou/yasaka_kanako/onbashira.py

533 lines
19 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 = 170.0
# Side panels have different lengths
side_length1: float = 200.0
side_length2: float = 350.0
side_length3: float = 400.0
side_length4: float = 400.0
side_thickness: float = 25.4 / 8
# Joints between two sets of side panels
angle_joint_thickness: float = 10.0
# Z-axis size of each angle joint
angle_joint_depth: float = 60.0
# Gap of each angle joint to connect the outside to the inside
angle_joint_gap: float = 10.0
angle_joint_bolt_length: float = 50.0
angle_joint_bolt_diam: float = 10.0
# Position of the holes, with (0, 0) being the centre of each side
angle_joint_bolt_position: list[float] = field(default_factory=lambda: [
(20, 10),
(60, 10),
])
angle_joint_flange_thickness: float = 7.8
angle_joint_flange_radius: float = 40.0
# Dimensions of gun barrels
barrel_diam: float = 25.4 * 1.5
barrel_length: float = 300.0
# Radius from barrel centre to axis
rotation_radius: float = 75.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 = 110.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 = 10.0
rotor_bind_radius: float = 85.0
stator_bind_radius: float = 140.0
material_side: Material = Material.WOOD_BIRCH
material_bearing: Material = Material.PLASTIC_PLA
material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT
material_brace: Material = Material.PLASTIC_PLA
def __post_init__(self):
assert self.n_side >= 3
# Bulk must be large enough for the barrel + bearing to rotate
assert self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam > self.rotation_radius + self.barrel_diam / 2
assert self.bearing_gap < 0.95 * self.bearing_ball_diam
assert self.rotor_bind_bolt_diam < self.rotor_bind_radius < self.bearing_track_radius
assert self.rotor_inner_radius < self.bearing_track_radius < self.stator_bind_radius
assert self.angle_joint_thickness > self.side_thickness
for (x, y) in self.angle_joint_bolt_position:
assert y < self.angle_joint_depth / 2
@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
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="angle-joint")
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.
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
"""
# 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)
)
hole_negative = Cq.Solid.makeCylinder(
radius=self.angle_joint_bolt_diam/2,
height=h,
pnt=(0,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
dri = self.bulk_radius - self.angle_joint_thickness
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"holeLPI{i}", (dri, x, py), direction="-X")
result.tagAbsolute(f"holeRPI{i}", (dri, 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")
result.tagAbsolute(f"holeLSI{i}", locrot * Cq.Location(dri, -x, py), direction="-X")
result.tagAbsolute(f"holeRSI{i}", locrot * Cq.Location(dri, -x, -py), direction="-X")
return result
@target(name="angle-joint-flanged")
def angle_joint_flanged(self) -> Cq.Workplane:
result = self.angle_joint()
th = math.pi / self.n_side
r = self.bulk_radius
flange = (
Cq.Sketch()
.push([
(r, r * math.tan(th))
])
.circle(self.angle_joint_flange_radius)
.reset()
.regularPolygon(self.side_width_inner, self.n_side, mode="i")
)
flange = (
Cq.Workplane()
.placeSketch(flange)
.extrude(self.angle_joint_flange_thickness)
.translate((0, 0, -self.angle_joint_flange_thickness/2))
)
ri = self.stator_bind_radius
h = self.angle_joint_flange_thickness
cyl = Cq.Solid.makeCylinder(
radius=self.rotor_bind_bolt_diam/2,
height=h,
pnt=(ri * math.cos(th), ri * math.sin(th), -h/2),
)
result = result + flange - cyl
result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z")
result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z")
return result
def 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_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
def assembly_ring(self, flanged=False) -> Cq.Assembly:
a = Cq.Assembly()
side = self.angle_joint_flanged() if flanged else self.angle_joint()
r = self.bulk_radius
for i in range(self.n_side):
a = a.addS(
side,
name=f"side{i}",
material=self.material_brace,
role=Role.CASING | Role.DECORATION,
loc=Cq.Location.rot2d(i*360/self.n_side),
)
return a
def assembly(self) -> Cq.Assembly:
a = Cq.Assembly()
a = (
a
.add(
self.assembly_section(length=self.side_length1, hasFrontHole=False, hasBackHole=True),
name="section1",
)
.add(
self.assembly_ring(flanged=True),
name="ring1",
)
.add(
self.assembly_section(length=self.side_length2, hasFrontHole=True, hasBackHole=True),
name="section2",
)
.add(
self.assembly_ring(),
name="ring2",
)
.add(
self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True),
name="section3",
)
.add(
self.assembly_ring(),
name="ring3",
)
.add(
self.assembly_section(length=self.side_length4, hasFrontHole=True, hasBackHole=False),
name="section4",
)
)
for (nl, nc, nr) in [
("section1", "ring1", "section2"),
("section2", "ring2", "section3"),
("section3", "ring3", "section4"),
]:
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"{nl}/side{i}?holeBSO{ih}",
f"{nc}/side{i}?holeLPM{ih}",
"Plane",
)
a = a.constrain(
f"{nr}/side{i}?holeFPO{ih}",
f"{nc}/side{i}?holeRSM{ih}",
"Plane",
)
a = a.add(self.assembly_rotor(), name="rotor")
return a.solve()