"""V1 drum project spec — derived from RUBISCO2 drum_v1.py.
V1 is the historical reference drum (700 mm OD × 2500 mm, 11 helical lifters,
gas engine + V-cradle support tires). V2 in the bible is the production
design (1100 × 4000 mm, electric, raceway band) — V1 stays for visual
comparison and the iterate-then-render demo loop.
Edit numerical params here to drive geometry; cadquery_part.py reads SPEC
on every run + Assembly.save() exports STEP/STL/GLB.
"""
SPEC = {
"slug": "v1-drum",
"title": "V1 drum (RUBISCO2 historical reference)",
"units": "mm",
# Drum body
"drum_od_mm": 700.0,
"drum_length_mm": 2500.0,
"shell_thickness_mm": 3.0,
# Welded segmented end rings
"end_ring_radial_proud_mm": 25.0,
"end_ring_thickness_mm": 8.0,
"end_ring_inset_mm": 50.0,
# Sheet overlap mid-ring
"mid_ring_radial_proud_mm": 15.0,
"mid_ring_thickness_mm": 6.0,
# Lifters — 11 helical shovels (short plates, spiral arrangement)
"lifter_count": 11,
"lifter_axial_plate_mm": 200.0,
"lifter_inward_height_mm": 60.0,
"lifter_thickness_mm": 4.0,
"lifter_x_start_mm": 250.0,
"lifter_x_step_mm": 200.0,
# Running bands — wide flat-bar where support tires contact
"back_running_band_x_frac": 0.20, # fraction of drum_length, near inlet/back support
"running_band_x_frac": 0.80, # fraction of drum_length, near discharge/front support
"running_band_axial_w_mm": 110.0,
"running_band_proud_mm": 6.0,
# Longitudinal stringers (basket structure)
"stringer_count": 8,
"stringer_w_mm": 25.0,
"stringer_h_mm": 5.0,
# Support tires (rubber, V-cradle)
"support_tire_radius_mm": 175.0,
"support_tire_width_mm": 90.0,
"support_tire_v_angle_deg": 25.0,
# Gas engine block (visual concept)
"engine_w_mm": 400.0,
"engine_d_mm": 300.0,
"engine_h_mm": 350.0,
"engine_x_frac": 0.78,
"engine_clearance_under_drum_mm": 300.0,
# Gearbox block (visual concept)
"gearbox_w_mm": 220.0,
"gearbox_d_mm": 200.0,
"gearbox_h_mm": 180.0,
"outputs": {
"step": "v1-drum.step",
"stl": "v1-drum.stl",
"glb": "v1-drum.glb",
},
}
"""V1 drum — CadQuery build, exports STEP/STL/GLB.
Reads numerical params from spec.py. Iterations to spec.py drive geometry.
Mirrors the architecture of RUBISCO2/Sand Separator/docs/v2/drum_v1.py but
uses cq.Assembly().save() for GLB (cadquery 2.7.0 verified Apr 29 — the
ExportTypes.GLTF attribute does not exist; Assembly.save infers from .glb
extension).
"""
from __future__ import annotations
import math
import sys
import cadquery as cq
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
from spec import SPEC
def build_assembly(s: dict = SPEC) -> cq.Assembly:
drum_od = s["drum_od_mm"]
drum_len = s["drum_length_mm"]
shell_t = s["shell_thickness_mm"]
drum_id = drum_od - 2 * shell_t
asm = cq.Assembly()
# Shell — perforated cylinder (perforations not modelled — visual only)
outer = cq.Workplane("YZ").cylinder(drum_len, drum_od / 2)
inner = cq.Workplane("YZ").cylinder(drum_len + 2, drum_id / 2)
shell = outer.cut(inner).translate((drum_len / 2, 0, 0))
asm.add(shell, name="V1 drum shell (3 mm perforated CS)",
color=cq.Color(0.45, 0.42, 0.38))
# End rings — upstream + downstream
er_proud = s["end_ring_radial_proud_mm"]
er_t = s["end_ring_thickness_mm"]
er_inset = s["end_ring_inset_mm"]
er_od = drum_od + 2 * er_proud
for x_pos, label in ((er_inset, "upstream"), (drum_len - er_inset, "downstream")):
outer = cq.Workplane("YZ").cylinder(er_t, er_od / 2)
inner = cq.Workplane("YZ").cylinder(er_t + 2, drum_od / 2)
ring = outer.cut(inner).translate((x_pos, 0, 0))
asm.add(ring, name=f"V1 {label} welded end ring",
color=cq.Color(0.40, 0.40, 0.40))
# Mid ring — sheet overlap join
mr_proud = s["mid_ring_radial_proud_mm"]
mr_t = s["mid_ring_thickness_mm"]
mr_od = drum_od + 2 * mr_proud
outer = cq.Workplane("YZ").cylinder(mr_t, mr_od / 2)
inner = cq.Workplane("YZ").cylinder(mr_t + 2, drum_od / 2)
mid_ring = outer.cut(inner).translate((drum_len / 2, 0, 0))
asm.add(mid_ring, name="V1 mid-ring (sheet overlap join)",
color=cq.Color(0.40, 0.40, 0.40))
# Running bands — wide rolled flat-bar, one at back/inlet and one near discharge
rb_positions = (
(drum_len * s["back_running_band_x_frac"], "back/inlet"),
(drum_len * s["running_band_x_frac"], "front/discharge"),
)
rb_w = s["running_band_axial_w_mm"]
rb_proud = s["running_band_proud_mm"]
rb_od = drum_od + 2 * rb_proud
for rb_x, rb_label in rb_positions:
outer = cq.Workplane("YZ").cylinder(rb_w, rb_od / 2)
inner = cq.Workplane("YZ").cylinder(rb_w + 2, drum_od / 2)
rb = outer.cut(inner).translate((rb_x, 0, 0))
asm.add(rb, name=f"V1 {rb_label} running band (wide flat-bar — support tires roll on this)",
color=cq.Color(0.32, 0.32, 0.34))
# Longitudinal stringers (basket structure)
n_str = s["stringer_count"]
str_w = s["stringer_w_mm"]
str_h = s["stringer_h_mm"]
str_axial = drum_len - 2 * er_inset
drum_radius = drum_od / 2
for i in range(n_str):
theta = i * 360.0 / n_str
bar = (cq.Workplane("XY").box(str_axial, str_w, str_h)
.translate((0, 0, drum_radius + str_h / 2))
.rotate((0, 0, 0), (1, 0, 0), theta)
.translate((drum_len / 2, 0, 0)))
asm.add(bar, name=f"V1 longitudinal stringer {i+1}/{n_str}",
color=cq.Color(0.42, 0.42, 0.44))
# Lifters — 11 short plates, helical
n_lift = s["lifter_count"]
lift_axial = s["lifter_axial_plate_mm"]
lift_inward = s["lifter_inward_height_mm"]
lift_t = s["lifter_thickness_mm"]
lift_x_start = s["lifter_x_start_mm"]
lift_x_step = s["lifter_x_step_mm"]
theta_step = 360.0 / n_lift
for i in range(n_lift):
plate_x = lift_x_start + i * lift_x_step
plate_theta = i * theta_step
plate = (cq.Workplane("XY").box(lift_axial, lift_t, lift_inward)
.translate((0, 0, drum_id / 2 - lift_inward / 2))
.rotate((0, 0, 0), (1, 0, 0), plate_theta)
.translate((plate_x, 0, 0)))
asm.add(plate,
name=f"V1 lifter {i+1}/{n_lift} @ X={plate_x:.0f}, θ={plate_theta:.0f}°",
color=cq.Color(0.55, 0.45, 0.30))
# Engine block + gearbox (visual)
eng_w = s["engine_w_mm"]; eng_d = s["engine_d_mm"]; eng_h = s["engine_h_mm"]
eng_x = drum_len * s["engine_x_frac"]
ground_z = -(drum_radius + s["engine_clearance_under_drum_mm"])
eng_y = -(drum_radius + 220)
eng_z = ground_z + eng_h / 2
engine = cq.Workplane("XY").box(eng_w, eng_d, eng_h).translate((eng_x, eng_y, eng_z))
asm.add(engine, name="V1 gas engine (7.5 HP, ~212cc — red Honda-style)",
color=cq.Color(0.78, 0.10, 0.10))
gb_w = s["gearbox_w_mm"]; gb_d = s["gearbox_d_mm"]; gb_h = s["gearbox_h_mm"]
gb_x = eng_x - eng_w/2 - gb_w/2 - 20
gearbox = cq.Workplane("XY").box(gb_w, gb_d, gb_h).translate((gb_x, eng_y, eng_z))
asm.add(gearbox, name="V1 gearbox (engine → chain → driven tire shaft)",
color=cq.Color(0.28, 0.28, 0.30))
# Support tires (V-cradle) — front pair plus added back pair
tire_r = s["support_tire_radius_mm"]
tire_w = s["support_tire_width_mm"]
v_deg = s["support_tire_v_angle_deg"]
r_band = drum_od / 2 + rb_proud
contact_r = r_band + tire_r
angle_rad = v_deg * math.pi / 180.0
for rb_x, rb_label in rb_positions:
for side, label in ((+1, "driven"), (-1, "passive")):
dy = side * contact_r * math.sin(angle_rad)
dz = -contact_r * math.cos(angle_rad)
cyl = cq.Workplane("YZ").cylinder(tire_w, tire_r).translate((rb_x, dy, dz))
asm.add(cyl, name=f"V1 {rb_label} {label} support tire (V-cradle {'+' if side > 0 else '-'})",
color=cq.Color(0.10, 0.10, 0.10))
return asm
def main():
asm = build_assembly(SPEC)
out = SPEC["outputs"]
asm.save(out["step"])
cq.exporters.export(asm.toCompound(), out["stl"], exportType="STL", tolerance=0.5)
asm.save(out["glb"]) # Assembly.save infers GLB from .glb extension (cq 2.7.0)
print(f"V1 drum: {SPEC['drum_length_mm']}×{SPEC['drum_od_mm']} mm OD, "
f"{SPEC['lifter_count']} lifters → step + stl + glb")
if __name__ == "__main__":
main()