Cosplay/nhf/touhou/yasaka_kanako/onbashira.py

1486 lines
52 KiB
Python

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,
)
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 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_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_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
# 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_COMMON.diam_thread
rotor_bind_radius: float = 82.0
rotor_bind_extra: int = 1
rotor_spacer_outer_diam: float = 15.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()
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
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_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="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("<Z").moveTo(
br * math.cos(-angle),
br * math.sin(-angle),
).tagPlane(f"holeB{i}", direction="-Z")
for j in range(1, 1 + self.stator_bind_extra):
angle2 = angle + (j + 1) * th2
result.faces("<Z").moveTo(
br * math.cos(-angle2),
br * math.sin(-angle2),
).tagPlane(f"holeE{i}", direction="-Z")
return result
@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.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(
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.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
@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
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_length/2)
a = a.addS(
self.barrel(),
name=f"barrel{i}",
material=self.material_barrel,
role=Role.DECORATION,
loc=loc_barrel,
)
loc = Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.rotor_bind_radius, 0, z)
#a = a.addS(
# self.stator_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.angle_joint_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_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
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) -> 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,
)
.addS(
self.handle(),
name="handle",
material=self.material_brace,
role=Role.HANDLE,
)
.add(self.assembly_machine(), name="machine")
)
# Add handle
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",
)
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",
)
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()