Cosplay/nhf/touhou/yasaka_kanako/onbashira.py

2199 lines
76 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
from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650
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: weigh
mass=0.0,
diam_thread=6.0,
pitch=1.0,
thickness=5.0,
width=9.89,
)
WASHER_COMMON = Washer(
# FIXME: weigh
mass=0.0,
diam_thread=6.0,
diam_outer=11.68,
thickness=1.5,
)
BOLT_COMMON = FlatHeadBolt(
# FIXME: weigh
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: weigh
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: weigh
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 Display(Model):
thickness: float = 2.5
length: float = 38.0
width: float = 12.0
@dataclass(frozen=True)
class FlangeCoupler(Model):
diam_thread: float = 8.0
diam_inner: float = 16.0
diam_outer: float = 32.0
height: float = 12.0
height_flange: float = 2.0
height_hole: float = 7.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)
)
hole_subtractor = Cq.Solid.makeCylinder(
radius=self.diam_thread_flange/2,
height=self.diam_inner,
pnt=(-self.diam_inner/2, 0, self.height_hole),
dir=(1, 0, 0)
)
result -= holes
result -= hole_subtractor
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")
result.tagAbsolute("dir", (0, 0, self.height_hole), direction="+X")
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_hole: float = 10.0
height_shaft: float = 13.0
height_base_shaft: float = 24.77
# Distance between anchor and the body
dx_anchor: float = 20.0
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_hole(self):
"""
Distance between mount point and shaft
"""
return self.height_hole + self.height_ring
@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)
)
)
hole_subtractor = Cq.Solid.makeCylinder(
radius=self.diam_thread/2,
height=self.diam_shaft,
pnt=(-self.diam_shaft/2, 0, self.height_body + self.height_hole),
dir=(1, 0, 0)
)
base_shaft = Cq.Solid.makeCylinder(
radius=self.diam_shaft/2,
height=self.height_base_shaft,
pnt=(0, 0, self.height_body),
)
shaft = Cq.Solid.makeCylinder(
radius=self.diam_shaft/2 * 0.9,
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) - hole_subtractor
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")
result.tagAbsolute("dir", (0, 0, self.height_body + self.height_hole), direction="+X")
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
battery_box: BatteryBox18650 = BatteryBox18650()
controller: ArduinoUnoR3 = ArduinoUnoR3()
# 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 = 50.0
motor_coupler_flange_thickness: float = 10.0
motor_coupler_flange_radius: float = 8.0
motor_coupler_height: float = 120.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",
"Axis"
)
.constrain(
"flange_coupler?dir",
"motor?dir",
"Plane",
param=0,
)
)
for i in range(self.flange_coupler.n_hole_flange):
j = self.flange_coupler.n_hole_flange - i - 1
a = a.constrain(
f"flange_coupler?holeB{i}",
f"driver_disk?holeB{j}",
"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("<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
@property
def rotor_radius(self) -> 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 = 7
t1 = 10
base_w = 17.0
theta = math.pi / self.n_side
theta2 = theta * 0.5
theta1 = theta * 1.3
cover_thickness = 4.0
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(theta1), r0 * math.sin(theta1)),
(r0 * math.cos(theta1), -r0 * math.sin(theta1)),
], 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(r2_5, mode="s")
.polygon([
(0, 0),
(r0 * math.cos(theta1), r0 * math.sin(theta1)),
(r0 * math.cos(theta1), -r0 * math.sin(theta1)),
], 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)
)
channel = (
Cq.Workplane()
.sketch()
.circle(self.rotation_radius+track_width/2)
.circle(self.rotation_radius-track_width/2, mode="s")
.finalize()
.extrude(t1)
.translate((0, 0, self.motor_seat_depth - t1))
)
channel_cover = (
Cq.Workplane()
.sketch()
.circle(self.rotation_radius+track_width/2)
.circle(self.rotation_radius-track_width/2, mode="s")
.polygon([
(0, 0),
(r0 * math.cos(theta1), r0 * math.sin(theta1)),
(r0 * math.cos(theta2), r0 * math.sin(theta2)),
], mode="i")
.finalize()
.extrude(cover_thickness)
.translate((0, 0, self.motor_seat_depth - cover_thickness))
)
# 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, mode="s")
.polygon([
(0, 0),
(r0 * math.cos(theta1), r0 * math.sin(theta1)),
(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(theta1), r0 * math.sin(theta1)),
(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(theta1), r0 * math.sin(theta1)),
(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
- channel
+ channel_cover
+ channel_cover.mirror("XZ")
)
# 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()