OpenSCAD
Sample code for designing python:
1. download Bezier example used for 3D contour seat (maths.scad, Render.scad)
2. Copy code below to your own <filename>.scad; the common modules can be kept in a separate file for reuse with new models, e.g. include <Renderer.scad> // disk pivot design from http://hpv.com.ua/2012/04/twist-python // Fixed version - bugs corrected from https://en.openbike.org/wiki/OpenSCAD
rider = true;
s = 1; // inch, 2.54 cm
pi = 3.14159265;
crank = 5.5 * s; // 140mm ring = 8 * s; tire = 1.5 * s; hub = 3 * s;
// Proportions bodyHeight = 10; armLength = 8; hand = [0.8, 0.6, 1.7]; // orientation
explode = 1; // 1 is normal, bigger "explodes" the parts.
// colors skin = [0.95, 0.75, 0.55]; c_tire = [0.4, 0.4, 0.4]; c_metal = [0.8, 0.8, 0.8];
a_seat = 30; // 20 to 70 degrees clock-wise
a_pivot = 60; a_front = 70; a_rear = a_front; // BUG FIX: was missing; a_rear was referenced but never defined a_BB = 85;
$fn = 20;
a_legs = [
[80, 69, 56, 42, 42, 63, 78, 84], [96, 88, 68, 30, 20, 52, 74, 90], [0, 0, 0, 45, 35, 20, 0, 0], [43, 63, 78, 86, 80, 69, 57, 42], [20, 52, 74, 92, 96, 88, 62, 30], [45, 22, 0, 10, 0, 0, 20, 30], [-45, 23, -45, 20, -45, 45, -30, 45], [-45, 45, -20, 45, -45, 45, -45, 45]
]; // for animation
front_wheel = 24 * s; // 16 to 26 rear_wheel = front_wheel;
wheelbase = 45 * s; // compromise bt weight ratio * compactness
BB2FWA = max(14 * s, front_wheel / 2 + 2 * s); xBB = BB2FWA * sin(a_BB); zBB = BB2FWA * cos(a_BB);
d = -(front_wheel - rear_wheel) / 2;
h_front = BB2FWA + BB2FWA;
z_pivot = 4 * s * sin(a_pivot) - BB2FWA * cos(a_front); // vertical location on pivot
pivot2RWA = wheelbase - BB2FWA * sin(a_front) - 4 * s * cos(a_pivot);
h_mid = 25;
c_frame = [1, 0, 0]; // color
// ----------- Modules -----------------
include <Renderer.scad> include <maths.scad>
function rem8(u, v) = round((u / v - floor(u / v)) * 8);
// BUG FIX: Added `w` as a named parameter to the wheel() module. // Previously the module had no parameters but was called as wheel(w=front_wheel). module wheel(w) {
rotate([90, 0, 0])
{
// spokes
color(c_metal)
%translate([0, 0, .75]) intersection() {
cylinder(h = 1, r1 = w / 2, r2 = 1, center = true);
for (alpha = [30, 90, 150, 210, 270, 330])
// BUG FIX: rotate() in 3D context requires a vector; was rotate($t*360+alpha)
rotate([0, 0, $t * 360 + alpha]) square([.25, w - 2 * tire], true);
}
%translate([0, 0, -.75]) intersection() {
cylinder(h = 1, r1 = 1, r2 = w / 2, center = true);
for (alpha = [0, 60, 120, 180, 240, 300])
// BUG FIX: same rotate() fix as above
rotate([0, 0, $t * 360 + alpha]) square([.25, w - 2 * tire], true);
}
// rim
color(c_metal)
difference() {
circle(w / 2 - tire + s / 5);
circle(w / 2 - tire - s / 10);
}
// hub
color(c_metal)
rotate([0, 0, 0]) roundedBox([2, 2, 4], 1, false);
// tire
color(c_tire)
rotate_extrude(convexity = 10, $fn = 50) {
translate([w / 2 - tire / 2, 0, 0])
circle(r = tire / 2, $fn = 30);
}
}
}
module BB() {
rotate([90, 0, 0])
{
// chain ring
color(c_metal)
translate([0, 0, 1.25 * s])
rotate_extrude() {
// BUG FIX: scale([.5,.05]) circle(ring) is not valid OpenSCAD.
// circle() takes a radius; the intent was a flattened torus cross-section.
// Fixed: translate then use circle with explicit radii.
translate([ring * 0.5, 0, 0])
scale([1, 0.1]) circle(r = ring * 0.5);
}
// crank
rotate([0, 0, (7 - rem8($t * 360, 90)) * 45])
{
color(c_metal)
{
translate([2 * s, 0, 2 * s]) square([6 * s, s], true);
translate([-2 * s, 0, -2 * s]) square([6 * s, s], true);
}
// pedals
color(c_tire)
{
translate([-5 * s, 0, -4 * s])
rotate([90, 0, 80 + (7 - rem8($t * 360, 90)) * 45 + a_legs[6][7 - rem8($t * 360, 90)]])
square([3 * s, 4 * s], true);
translate([5 * s, 0, 4 * s])
rotate([90, 0, 70 + (7 - rem8($t * 360, 90)) * 45 + a_legs[7][7 - rem8($t * 360, 90)]])
square([3 * s, 4 * s], true);
}
}
// BB axle
color(c_metal) cylinder(h = 4 * s, r = s, center = true, $fn = 20);
}
}
// rider based on groom from http://www.thingiverse.com/thing:3495 module veloRider() {
// torso roundedBox([8, 5, bodyHeight + 2], 2.2, false);
// rib cage translate([0, 0.1, 9 * 0.75 / 2]) roundedBox([9, 5.1, bodyHeight * 0.75 + 2], 2.5, false);
// chest translate([0, 0.1, 5]) roundedBox([9.5, 5.3, bodyHeight * 0.5 + 2], 2.0, false);
translate([0, 0, bodyHeight / 2 + 0.5])
{
// arms
mirror([1, 0, 0]) rotate([-45, 0, 0]) arm(1.4, 3.5, armLength, 170, -15, 115, hand);
translate([0, 0, 0.5]) rotate([-45, 0, 0]) arm(1.4, 3.5, armLength, 170, -15, 115, hand);
rotate([-a_seat - 10, 0, 0])
{
// neck
color(skin) translate([0, 0, 2]) cylinder(r1 = 1.3, r2 = 1.4, h = 2);
// head
color(skin)
translate([0, 0, 6.5 + 5 * (explode - 1)])
rotate([-4, -1, 0]) scale([2.4, 2.4, 3.2]) sphere($fn = 30);
// helmet
color(c_tire) translate([0, -5, 1.8 + 15 * (explode - 1)])
{
helmet();
visor();
}
}
}
// legs
color(c_tire) translate([0, -0.2, -bodyHeight / 2 + 0.5])
{
mirror([1, 0, 0]) leg(2, 2, armLength * 1.1, 2,
a_legs[0][7 - rem8($t * 360, 90)],
a_legs[1][7 - rem8($t * 360, 90)],
[a_legs[2][7 - rem8($t * 360, 90)], 0, 0]);
leg(2, 2, armLength * 1.1, 2,
a_legs[3][7 - rem8($t * 360, 90)],
a_legs[4][7 - rem8($t * 360, 90)],
[a_legs[5][7 - rem8($t * 360, 90)], 0, 0]);
}
} // BUG FIX: closing brace for veloRider() was missing; the module was never closed.
module helmet() {
translate([0, 5, 5.5])
difference() {
sphere(3, center = true);
translate([0, 0, -5.5]) cube(10, center = true);
}
}
module visor() {
translate([4.5,-1,12]) rotate([30,0,180]) scale(3) linear_extrude_bezier(
[[[0.7,-4,-.6], [1,-5,0.0], [2,-5,0.0], [2.3,-4,-.6]], [[.3,-2.5,-1], [.7,-2.5,0.4],
[2.3,-2.5,0.4], [2.7,-2.5,-1]], [[.9,-2.3,-1.2], [1,-2.1,0.6], [2,-2.1,0.6],
[2.1,-2.3,-1.2]], [[1.2,-2.1,-1.4], [1,-2,-1], [2,-2,-1], [1.8,-2.1,-1.4]]],
steps=10, thickness = 0.05*s);
}
module leg(thickness, hipWidth, armLength, legSpread, kneeLift, kneeBend, footPos) {
translate([4 * (explode - 1), 0, -10 * (explode - 1)])
{
// upper leg
translate([hipWidth, 0, 0]) rotate([-kneeLift, 180 - legSpread, 0])
{
sphere(r = thickness * 1.3);
cylinder(r1 = thickness * 1.2, r2 = thickness * 0.8, h = armLength);
// joint
translate([0, 0, armLength + 10 * (explode - 1)])
{
// lower leg
sphere(r = thickness * 0.8);
rotate([kneeBend, 0, 0])
{
cylinder(r1 = thickness * 0.8, r2 = thickness * 0.5, h = armLength);
// foot
translate([0, 1.5, 1 + armLength + 10 * (explode - 1)])
rotate([footPos[0], footPos[1], footPos[2]])
roundedBox([2.5, 6, 2], thickness / 2);
}
}
}
}
}
module arm(thickness, shoulderWidth, armLength, armBend, elbowBend, elbowBendForward, hand) {
translate([10 * (explode - 1), 0, 0])
{
// upper arm
translate([shoulderWidth, 0, 0]) rotate([0, armBend, 0])
{
sphere(r = thickness * 1.3);
cylinder(r1 = thickness * 1.2, r2 = thickness * 0.8, h = armLength);
// joint
translate([0, 0, armLength + 10 * (explode - 1)])
{
sphere(r = thickness * 0.8);
rotate([-elbowBendForward, -elbowBend, 0])
{
cylinder(r1 = thickness * 0.8, r2 = thickness * 0.6, h = armLength * 0.8);
// hand
color(skin)
translate([0, 0, armLength * 0.8 + 10 * (explode - 1)])
scale([hand[0], hand[1], hand[2]]) sphere(r = thickness);
}
}
}
}
}
// size is a vector [w, h, d] module roundedBox(size, radius, sidesonly) {
rot = [[0, 0, 0], [90, 0, 90], [90, 90, 0]];
if (sidesonly) {
cube(size - [2 * radius, 0, 0], true);
cube(size - [0, 2 * radius, 0], true);
for (x = [radius - size[0] / 2, -radius + size[0] / 2],
y = [radius - size[1] / 2, -radius + size[1] / 2]) {
translate([x, y, 0]) cylinder(r = radius, h = size[2], center = true);
}
} else {
cube([size[0], size[1] - radius * 2, size[2] - radius * 2], center = true);
cube([size[0] - radius * 2, size[1], size[2] - radius * 2], center = true);
cube([size[0] - radius * 2, size[1] - radius * 2, size[2] ], center = true);
for (axis = [0:2]) {
for (x = [radius - size[axis] / 2, -radius + size[axis] / 2],
y = [radius - size[(axis+1)%3] / 2, -radius + size[(axis+1)%3] / 2]) {
rotate(rot[axis])
translate([x, y, 0])
cylinder(h = size[(axis+2)%3] - 2 * radius, r = radius, center = true);
}
}
for (x = [radius - size[0] / 2, -radius + size[0] / 2],
y = [radius - size[1] / 2, -radius + size[1] / 2],
z = [radius - size[2] / 2, -radius + size[2] / 2]) {
translate([x, y, z]) sphere(radius);
}
}
}
module seat() {
translate([0, -6 * s, 0]) rotate([-90, 0, 90]) scale(4)
linear_extrude_bezier([
[[.7, -6, -1], [1, -6.5, -1.5], [2, -6.5, -1.5], [2.3, -6, -1]],
[[0, -5, 1], [1, -5, 1], [2, -5, 1], [3, -5, 1]],
[[0, -4, -.5], [1, -4, 0], [2, -4, 0], [3, -4, -.5]],
[[0, 0, 0], [1, 0.5, 0.5], [2, 0.5, 0.5], [3, 0, 0]]
], steps = 10, thickness = 0.2 * s);
translate([0, -6 * s, 0]) rotate([-120, 0, 90]) scale(4)
linear_extrude_bezier([
[[0.3, 2.5, 0.5], [1, 3.5, 0], [2, 3.5, 0], [2.7, 2.5, 0.5]],
[[0, 2, 1], [1, 2, 1], [2, 2, 1], [3, 2, 1]],
[[0, 1, 1], [1, 1, 1], [2, 1, 1], [3, 1, 1]],
[[0, 0, 0], [1, 0, .5],[2, 0, .5], [3, 0, 0]]
], steps = 10, thickness = 0.25 * s);
}
module python01() {
wheel(w = front_wheel);
translate([-BB2FWA / 2 * sin(a_rear), 0, -BB2FWA / 2 * cos(a_rear)]) rotate([0, a_front, 0]) color(c_frame) // BUG FIX: was a_rear; rear module should use a_front for the fork angle roundedBox([1.5 * s, 0, BB2FWA + s], 0.75 * s, false);
}
module python02() {
wheel(w = front_wheel);
// front tube translate([-BB2FWA / 2 * sin(a_front), 0, -BB2FWA / 2 * cos(a_front)]) rotate([0, a_front, 0]) color(c_frame) roundedBox([1.5 * s, 0, BB2FWA + s], 0.75 * s, false);
translate([BB2FWA / 2 * sin(a_BB), 0, BB2FWA / 2 * cos(a_BB)]) rotate([0, a_BB, 0]) color(c_frame) roundedBox([1.5 * s, 0, BB2FWA + s], 0.75 * s, false);
translate([xBB, 0, zBB]) BB();
}
// -------------- Assembly -------------
rotate([0, 0, $t * 360]) translate([wheelbase / 2, 0, 0]) {
// rear translate([-BB2FWA - h_mid - 8 * s * cos(a_pivot), 0, 0]) rotate([0, 0, 180]) translate([BB2FWA, 0, 0]) python01();
// front python02();
// front pivot color(c_metal) translate([-BB2FWA * sin(a_front) - 3, 0, -7]) rotate([0, a_pivot - 90, 0]) roundedBox([6.5 * s, 6.5 * s, 1 * s], 0.375 * s, false);
// middle tube
color(c_frame)
{
translate([-wheelbase + pivot2RWA - h_mid / 2 - 4, 0, -6.5])
rotate([0, 98, 0])
roundedBox([1.5 * s, 0 * s, h_mid + 1 * s], 0.75 * s, false);
}
// rider & seat
translate([-47 * s + BB2FWA + crank, 0, z_pivot + 5 * s])
{
if (rider)
{
scale(2 * s)
rotate([90 - a_seat, 0, -90])
veloRider();
}
// BUG FIX: seat rendering was inside the veloRider() module definition
// due to the missing closing brace. It belongs here in the assembly.
% rotate([0, a_seat - 90, 0])
translate([-6 * s, 0, -5.5 * s]) seat();
}
// platform (debug) %translate([-wheelbase / 2 - 5 * s, 0, -front_wheel / 2]) cylinder(h = .1, r = wheelbase / 2 + front_wheel / 2, center = true);
}