Files
busbar-designer/scad/train_tracks.scad
T
wenil 102cfcee64 tracks: new /tracks generator for IKEA Lillabo / Brio train tracks
Wraps torwanbukaj/ikea-brio-others-compatible-train-tracks-generator
( https://github.com/torwanbukaj/... ) as a third generator alongside
holder and the universal scad playground.

Approach
--------
The upstream .scad ships as "library + one active example call". Used
include<> (rather than use<>) so the library's top-level globals
(track_width, plug/nest dimensions, $fn = 150, etc.) are available
to the modules — `use<>` does NOT propagate variables. Commented out
the upstream `track_tester();` call so include<> doesn't also emit
the tester geometry every time.

Per-render the backend builds a tiny wrapper SCAD on the fly:

    include <scad/train_tracks.scad>
    track(length=100, end1="plug", end2="nest", cutout=true, ...);

and hands it to holder.render_source().

Part types exposed (9)
----------------------
- tester        Calibration block (15-60mm)
- track         Straight (length + chamfers + grooves + plug/nest ends)
- arc           Curved (radius + angle, IKEA/Brio/J'adore radii in help text)
- dogbone       Nest-nest connector
- intersection  Crossing (angle + 2 lengths + 4 ends)
- switch        Turnout (left/right radius+angle, straight branch, common end)
- snake         S-curve (raised default target_length to 200 — the upstream
                modules's natural curvy-span at angle=45/radius=86 is ~150
                and the assert fires below that)
- adapter       BRIO <-> IKEA system adapter (plug + nest side)
- bridge        Multi-part (overview/ground/slope/pillar — value_map
                translates the UI labels to the int 0..3 the SCAD wants)

Files
-----
- scad/train_tracks.scad      Downloaded upstream (57 999 bytes), only
                              modification is the // before the top-level
                              track_tester() call.
- tracks.py                   PART_SCHEMAS dict, build_wrapper_scad,
                              render() that delegates to render_source.
- app.py                      GET /api/tracks/params, POST /api/tracks/render,
                              GET /tracks page route.
- static/tracks.html          Page with part selector + dynamic param form +
                              shared viewer markup. Reuses holder.css.
- static/js/tracks-app.js     Controller. Switching the part select redraws
                              the form (each part has its own schema).
                              Ctrl+Enter renders, Cancel uses AbortController.

Nav
---
Tracks link added to topbar on holder / index / scad.

Smoke test
----------
All 9 parts render with default params on the Manifold backend in
under 0.5s each (output sizes 440 KB - 1.8 MB).
2026-05-25 13:38:29 +03:00

1522 lines
57 KiB
OpenSCAD

// Train tracks generator for IKEA Lillabo, Brio and others
//// by torwan @ January 8th, 2023
// Description / Hints:
// - See more details on https://www.thingiverse.com/thing:5598668
// - All dimensions in [mm].
// - Before printing any "bigger" part generated by this generator,
// I highly recommend to print a tester-track relevant to the system
// you posses. Use the "track_tester()" module to generate a tester.
// - To print STL file, render the object using F6 and save
// an STL file using F7.
// - Standard lengths of the IKEA Lillabo straight tracks (without
// a plug): 50mm, 100mm, 146mm, 205mm, 240mm.
// - Standard arcs in the IKEA Lillabo tracks family (original tracks
// measured): 45deg + inner radius 185mm which gives U-turn inner
// diameter 370mm, 22.5deg + inner radius 180mm which gives U-turn
// inner diameter 360mmm.
// - Arc parameters used in J'adore system (sold e.g., by Rossmann):
// 45deg, inner radius 86mm, => U-turn inner diameter 172mm
// - Standard lengths of the Brio straight tracks (without a plug):
// 145mm.
// - $fn controls number of a circle fragments. I recommend to use
// $fn = 300;, especially for larger arcs and bridge parts. Keep
// in mind that the larger value of $fn, the longer rendering will
// take, but also the smoother "roundy" parts of your tracks will
// be. When tinkering around and playing with different parameters
// values, I recommend using smaller $fn value unless you have rally
// fast CPU/GPU PC configuration.
//$fn = 300;
$fn = 150;
// ### General track dimensions
// Track dimensions (based on original IKEA Lillabo and BRIO tracks).
// You should not change these values unless you are going to tune this
// generator to some really different system than the ones available
// commonly on the market.
track_width = 40;
track_height = 12;
track_well_depth = 2.9;
track_well_width_top = 6.75;
track_well_width_bottom = 4.75;
track_well_spacing = 25.7;
track_chamfer = 1.5;
// ### DO NOT CHANGE BELOW DEFINITIONS UNLESS IT IS REALLY NECESSARY
// ### (use the "general" variables to play with various dimensions instead)
// Track Plug IKEA Lillabo (do not change)
ikea_track_plug_radius = 6.25; // 6.25
ikea_track_plug_radius_ext = 6.5; // 6.5 for a dog-bone with springy plug
ikea_track_plug_neck_width = 6.1; // 6.1
ikea_track_plug_neck_length = 11; // 11
ikea_track_plug_txt = "I";
// Track Nest IKEA Lillabo (do not change)
ikea_track_nest_radius = 6.4; // 6.4
ikea_track_nest_neck_width = 6.3; // 6.3
ikea_track_nest_neck_length = 11; // 11
ikea_track_nest_txt = "I";
// Track Plug Brio (do not change)
brio_track_plug_radius = 6.25; // 6.25
brio_track_plug_radius_ext = 6.5; // 6.5 for a dog-bone with springy plug
brio_track_plug_neck_width = 6.1; // 6.1
brio_track_plug_neck_length = 12; // 12
brio_track_plug_txt = "B";
// Track Nest Brio (do not change)
brio_track_nest_radius = 6.4; // 6.4
brio_track_nest_neck_width = 6.3; // 6.3
brio_track_nest_neck_length = 12; // 12
brio_track_nest_txt = "B";
// Bridge components (do not change)
bridge_pillar_depth = 10;
slope_straight_nest_length = 20; //length of the straight ending (with nest)
slope_straight_plug_length = 10; //length of the straight ending (without plug)
// Helpers (do not change)
tr_wl_w_stick_out = (track_well_width_top-track_well_width_bottom)/2;
tr_co_w = 0.75 * (track_well_spacing-track_well_width_top); // track cutout width
cc = 0.001;
switch_cut_ext = 1;
// ### USE BELOW VARIABLES TO CHOOSE YOUR TARGET SYSTEM
// ### Below variables are used by majority of modules in this library.
// ### Feel free to define your own values here instead of using
// ### pre-defined parameters for specific systems.
// ### For example: track_plug_radius = 6.45;
// Track Plug - choose your system here or define your own values
track_plug_radius = ikea_track_plug_radius;
track_plug_radius_ext = ikea_track_plug_radius_ext;
track_plug_neck_width = ikea_track_plug_neck_width;
track_plug_neck_length = ikea_track_plug_neck_length;
track_plug_txt = ikea_track_plug_txt;
// Track Nest - choose your system here or define your own values
track_nest_radius = ikea_track_nest_radius;
track_nest_neck_width = ikea_track_nest_neck_width;
track_nest_neck_length = ikea_track_nest_neck_length;
track_nest_txt = ikea_track_nest_txt;
// ### TESTING / GENERATION AREA / EXAMPLES
// ### Below I have provided many examples of how to call a generator module.
// ### They should help to learn quickly how to generate a desired model.
// ### When trying different module, always remember to comment back
// ### a previously used one :)
// ### If you have any doubts regarding some parameters, see detailed description
// ### provided in the front of each module definition.
// ---------------------------------
//TRACK TESTER
// NB: this top-level call was commented out by busbar-designer so the file
// can be `include`d as a library without emitting any geometry. The /tracks
// page wraps and calls the right module explicitly.
//track_tester();
// ---------------------------------
// TRACKS ADAPTER
//tracks_adapter(length = 30, nest = "B", plug = "I");
//tracks_adapter(length = 30, nest = "I", plug = "B");
// ---------------------------------
// STRAIGHT track examples
/*
track(length = 100,
end1 = "plug",
end2 = "nest",
cutout = true);
*/
/*
track(length = 25,
cutout = true,
end1 = "plug",
end2 = "nest",
part_chamfers = true,
grooves = true);
*/
/*
track(length = 50,
cutout = true,
end1 = "plug",
end2 = "nest",
part_chamfers = true,
grooves = true);
*/
// ---------------------------------
// ARC track examples
//track_arc();
//track_arc(angle = 90, radius = 86, both_sides = true);
//track_arc(angle = 45, radius = 185, both_sides = true);
//track_arc(angle = 22.5, radius = 180);
//track_arc(angle = -45, radius = 185, cutout = false, both_sides = false);
/*
track_arc(angle = -90,
radius = 86,
end1 = "nest",
end2 = "plug",
grooves = true,
both_sides = true,
cutout = true,
cutout_corr = 0,
part_chamfers = true);
*/
/*
track_arc(angle = -45,
radius = 86,
end1 = "nest",
end2 = "plug",
grooves = true,
both_sides = true,
cutout = true,
cutout_corr = 0,
part_chamfers = true);
*/
/*
track_arc(angle = 45,
radius = 86,
end1 = "nest",
end2 = "nest",
grooves = true,
both_sides = true,
cutout = true,
cutout_corr = 0,
part_chamfers = true);
*/
/*
track_arc(angle = -45,
radius = 185,
end1 = "nest",
end2 = "nest",
grooves = true,
both_sides = false,
cutout = true,
cutout_corr = 0,
part_chamfers = true);
*/
/*
track_arc(radius = 86, angle = -90,
end1 = "plug", end2 = "plug",
grooves = true,
both_sides = true,
cutout = true,
cutout_corr = 15,
part_chamfers = true);
*/
// ---------------------------------
// DOGBONE
//track_dogbone();
// ---------------------------------
// INTERSECTION examples
/*
intersection(angle = 90,
lengthA = 75, // 75mm is too short for Brio
lengthB = 75, // 75mm is too short for Brio
both_sides = true);
*/
/*
intersection(angle = 45,
lengthA = 146,
lengthB = 146,
end1A = "nest",
end2A = "plug",
end1B = "nest",
end2B = "plug",
both_sides = true);
*/
/*
intersection(angle = 90,
lengthA = 100,
lengthB = track_width,
end1A = "nest",
end2A = "plug",
end1B = "plug",
end2B = "plug",
both_sides = true);
*/
// ---------------------------------
// BRIDGE set examples
// - what_to_generate - controls what is generated by the module,
// allowed values:
// -- 0 (zero) - shows a bridge overview with calculation of
// the bridge height
// -- 1 - shows ONLY a bridge ground part
// -- 2 - shows ONLY a bridge slope (upper) part
// -- 3 - shows ONLY a bridge pillar part
// Showcase 15 degrees, radius 100mm, 205mm straight part, 50mm pillar length:
/*
generate_bridge(what_to_generate = 0,
bridge_angle = 15,
slope_radius = 100,
straight_part_l = 205,
pillar_l = 50,
cutout = true);
*/
// Showcase 20 degrees, radius 200mm, 146mm straight part, 50mm pillar length:
/*
generate_bridge(what_to_generate = 1,
bridge_angle = 20,
slope_radius = 200,
straight_part_l = 146,
pillar_l = 50,
cutout = true);
*/
// Showcase 14 degrees, radius 100mm, 205mm straight part, 50mm pillar length:
//generate_bridge(what_to_generate = 0);
// ---------------------------------
// SNAKE track examples
/*
snake_track(angle = 20,
radius = 186,
target_length = 150,
end1 = "nest",
end2 = "plug",
cutout = false,
both_sides = false,
cutout_corr = 0);
*/
/*
snake_track(angle = 35.775,
radius = 86,
cutout = true,
cutout_corr = 2,
target_length = 146,
both_sides = true,
end1 = "nest",
end2 = "plug");
*/
/*
snake_track(angle = 70,
radius = 80,
target_length = 300,
end1 = "nest",
end2 = "plug",
cutout = false,
both_sides = false,
cutout_corr = 0);
*/
// ---------------------------------
// SWITCH track examples
// If a model does not look good in the preview window, render it using F6
/*
switch(angleR = 45, radiusR = 86, endR = "nest",
angleL = 0, radiusL = 0, endL = "nest",
lengthS = 100, endS = "nest",
endCommon = "plug",
both_sides = true);
*/
/*
switch(angleR = 45, radiusR = 86, endR = "nest",
angleL = 45, radiusL = 86, endL = "nest",
lengthS = 0, endS = "none",
endCommon = "nest",
both_sides = true);
*/
/*
switch(angleR = 45, radiusR = 86, endR = "nest",
angleL = 45, radiusL = 86, endL = "plug",
lengthS = 146, endS = "nest",
endCommon = "nest",
both_sides = true);
*/
/*
switch(angleR = 45, radiusR = 86, endR = "plug",
angleL = 45, radiusL = 86, endL = "plug",
lengthS = 146, endS = "nest",
endCommon = "nest",
both_sides = true);
*/
/*
switch(angleR = 45, radiusR = 86, endR = "nest",
angleL = 45, radiusL = 86, endL = "nest",
lengthS = 100, endS = "nest",
endCommon = "nest",
both_sides = true);
*/
/*
switch(angleR = 90, radiusR = 86, endR = "plug",
angleL = 45, radiusL = 86, endL = "plug",
lengthS = 100, endS = "plug",
endCommon = "nest",
both_sides = true);
*/
// ########## End of testing area ##########
// ### TRACK TESTER MODULE
// Input parameters:
// - tester_length - length of a tester without a plug
module track_tester(tester_length = 25) {
difference() {
translate([0, 0, -track_height + track_well_depth + 0.4])
track(length = tester_length,
cutout = true,
end1 = "nest",
end2 = "plug",
part_chamfers = true,
grooves = true);
translate([-cc, -cc, -track_height])
cube([2*tester_length, 2*cc + track_width , track_height]);
}
}
// ### SWITCH TRACK GENERATOR MODULE
// Railway switch() module allows to generate various types of switches.
// You can independently control if it has left and/or straight and/or
// right leg. You can control what kind of ending each of leg has.
// Input parameters:
// - angleR - desired angle of the right turn
// - radiusR - desired radius of the right turn, zero disables the leg
// - endR - desired end type of the right leg ("plug", "nest" or "none")
// - angleL, radiusL, endL - left leg parameters
// - lengthS - length of the middle, straight leg
// - endCommon - common end type "plug", "nest" or "none")
// - both_sides - controls if the grooves should be generated only on
// the top or also on the bottom:
// -- true - grooves on both sides
// -- false - grooves only on the top
module switch(angleR = 45, radiusR = 86, endR = "nest",
angleL = 45, radiusL = 86, endL = "plug",
lengthS = 146, endS = "nest",
endCommon = "nest",
both_sides = true) {
difference() {
union() { // Left + Straight + Right main bodies
// Arc to the left
if (radiusL > 0) {
track_arc(angle = angleL,
radius = radiusL,
part_chamfers = true,
grooves = false,
both_sides = false,
cutout = false,
end1=endCommon, end2="none");
}
// Arc to the right
if (radiusR > 0) {
track_arc(angle = -angleR,
radius = radiusR,
part_chamfers = true,
grooves = false,
both_sides = false,
cutout = false,
end1=endCommon, end2="none");
}
// Straight part
if (lengthS > 0) {
translate([track_width, 0, 0])
rotate([0, 0, 90])
track(length = lengthS,
cutout = false,
part_chamfers = true,
grooves = false,
end1 = endCommon, end2 = endS);
}
} //union
// grooves and the nest (if requested) on the left turn
if (radiusL > 0) {
translate([-radiusL, 0, 0])
rotate_extrude(angle = angleL, convexity = 10)
translate([radiusL, 0 , 0])
grooves(both_sides);
// Nest if requested
if (endL == "nest") {
translate([-radiusL , 0, 0])
rotate([0, 0, angleL])
translate([radiusL + track_width/2, 0, 0])
rotate([0, 0, -90])
track_nest();
}
// When the left turn is too "short", and the straight part
// is requested, the right turn ends up inside the straight part.
// In such a case, additional cutting is required to provide
// sufficient amount of space for adjacent track.
translate([-radiusL , 0, 0])
rotate([0, 0, angleL])
rotate_extrude(angle = angleL/4, convexity = 10)
translate([radiusL, 0, 0])
translate([-switch_cut_ext/2, 0, 0])
square([track_width + switch_cut_ext, track_height]);
} //(radiusL > 0)
// grooves and the nest (if requested) on the right turn
if (radiusR > 0) {
translate([radiusR+track_width, 0, 0])
rotate_extrude(angle = -angleR, convexity = 10)
translate([-radiusR-track_width, 0 , 0])
grooves(both_sides);
// Nest if requested
if (endR == "nest") {
translate([radiusR+track_width, 0, 0])
rotate([0, 0, -angleR])
translate([-radiusR-track_width/2, 0, 0])
rotate([0, 0, -90])
track_nest();
}
// When the right turn is too "short", and the straight part
// is requested, the right turn ends up inside the straight part.
// In such a case, additional cutting is required to provide
// sufficient amount of space for adjacent track.
translate([radiusR+track_width, 0, 0])
rotate([0, 0, -angleR])
rotate_extrude(angle = -angleR/4, convexity = 10)
translate([-radiusR-track_width, 0, 0])
translate([-switch_cut_ext/2, 0, 0])
square([track_width + switch_cut_ext, track_height]);
} // (radiusR > 0)
// grooves on the straight part
if (lengthS > 0) {
translate([0, lengthS, 0])
rotate([90, 0, 0])
linear_extrude(lengthS)
grooves(both_sides);
}
} //main difference
// Generating plugs
if (endL == "plug") {
translate([-radiusL, 0, 0])
rotate([0, 0, angleL])
translate([radiusL + track_width/2, 0, 0])
rotate([0, 0, 90])
track_plug();
}
if (endR == "plug") {
translate([radiusR+track_width, 0, 0])
rotate([0, 0, -angleR])
translate([-radiusR - track_width/2, 0, 0])
rotate([0, 0, 90])
track_plug();
}
}
// ### ARC TRACK GENERATOR MODULE
// track_arc() module allows to generate various types of arcs.
// You can control angle and radius of generated arcs.
// Input parameters:
// - angle - desired angle of an arc:
// -- negative values - arc turns right
// -- positive values - arc turns left
// - radius - desired radius of the arc
// - end1, end2 - desired end type of the arc ("plug", "nest" or "none")
// - grooves - enables or disables generation of grooves:
// -- true - grooves enabled
// -- false - grooves disabled
// - cutout - controls if there should be a cutout inside the arc body,
// this is to help the part to "lose some weight"
// -- true - cutout enabled
// -- false - cutout disabled
// - cutout_corr - additional angle offset (in degrees) which will be
// used by the generator for the cutout starting and ending angle,
// in general only positive values make sense to shorten the cutout
// angular length
// - both_sides - controls if the grooves should be generated only on
// the top or also on the bottom:
// -- true - grooves on both sides
// -- false - grooves only on the top
// - part_chamfers - controls if the part should have chamfers:
// -- true - chamfers enabled
// -- false - chamfers disabled
module track_arc(radius = 185, angle = 45,
end1 = "plug", end2 = "nest",
grooves = true,
both_sides = false,
cutout = true,
cutout_corr = 0, //cutout start/end correction angle
part_chamfers = true) {
radius_corr = (angle > 0) ? radius : -radius - track_width;
angle_abs = abs(angle);
angle_dir = (angle >= 0) ? 1 : -1;
a = radius+(track_width/2);
b = track_nest_neck_length + track_nest_radius;
cutout_start_angle = (end1 == "nest") ? cutout_corr + atan((tr_co_w + b) / a) : cutout_corr + atan(tr_co_w / a);
cutout_end_angle = (end2 == "nest") ? cutout_corr + atan((tr_co_w + b) / a) : cutout_corr + atan(tr_co_w /a);
// echo(str(cutout_start_angle));
// echo(str(cutout_end_angle));
translate([-radius_corr, 0, 0]) {
difference() {
rotate_extrude(angle = angle, convexity = 10)
translate([radius_corr, 0 , 0])
track_blueprint(grooves, both_sides, part_chamfers);
if (end1 == "nest") {
rotate([0, 0, 0])
translate([radius_corr + track_width/2, 0, 0])
rotate([0, 0, 90])
track_nest();
}
if (end2 == "nest") {
rotate([0, 0, angle])
translate([radius_corr + track_width/2, 0, 0])
rotate([0, 0, -90])
track_nest();
}
// Cutout
if (cutout) {
if ((angle_abs-cutout_end_angle-cutout_start_angle) > 0) {
rotate([0, 0, angle_dir*cutout_start_angle]) {
rotate_extrude(angle = angle-angle_dir*(cutout_start_angle+cutout_end_angle), convexity = 10)
translate([radius_corr+track_width/2 - tr_co_w/2, -cc , 0])
square([tr_co_w, track_height + 2*cc]);
translate([radius_corr+track_width/2, -cc , 0])
cylinder(d = tr_co_w, h = track_height + 2*cc);
}
rotate([0, 0, angle-angle_dir*cutout_end_angle]) {
translate([radius_corr+track_width/2, -cc , 0])
cylinder(d = tr_co_w, h = track_height + 2*cc);
}
} //if (minimum length)
} //if (cutout)
}
// Generating plugs
if (end1 == "plug") {
translate([radius_corr + track_width/2, 0, 0])
rotate([0, 0, -90])
track_plug();
}
if (end2 == "plug") {
rotate([0, 0, angle])
translate([radius_corr + track_width/2, 0, 0])
rotate([0, 0, 90])
track_plug();
}
}
}
// ### SNAKE TRACK GENERATOR MODULE
// Snake_track() module allows to generate various types of snake tracks.
// Snake track consists of two adjacent arcs extended by two straight
// tracks on both sides to reach total target length (along Y axis).
// It can only shift the axis to the right, there is no option to generate
// a snake track shifting to the left (TO DO in the future). Print it
// with "both_sides = true" to get the grooves also on the bottom and you
// can flip the part up-side-down to get the snake track shifting to the left.
// Input parameters:
// - angle - desired angle of a singular arc used to create the snake track,
// only positive values make sense
// - radius - desired radius of a singular arc the used to create a snake
// track
// - end1, end2 - desired end type of the snake track ("plug", "nest" or "none")
// - cutout - controls if there should be cutouts inside the snake track body,
// this is to help the part to "lose some weight"
// -- true - cutout enabled
// -- false - cutout disabled
// - cutout_corr - additional angle offset (in degrees) which will be
// used by the generator for the cutout starting and ending angle for
// each arc part used to create the snake track, in general only positive
// values make sense
// - both_sides - controls if the grooves should be generated only on
// the top or also on the bottom:
// -- true - grooves on both sides
// -- false - grooves only on the top
module snake_track(angle = 45,
radius = 86,
cutout = true,
cutout_corr = 0,
target_length = 146,
both_sides = true,
end1 = "plug",
end2 = "nest") {
angle = (angle <= 90) ? angle : 90;
h1 = sin(angle) * radius;
h2 = sin(angle) * (radius + track_width);
hf = tan(angle) * (radius + track_width);
hf1 = hf - h1;
hf2 = hf - h2;
beta = 180 - 90 - angle;
//l1 = tan(beta) * (hf - h1);
//l2 = tan(beta) * (hf - h2);
l1 = (angle != 90) ? tan(beta) * (hf - h1) : radius + track_width;
l2 = (angle != 90) ? tan(beta) * (hf - h2) : radius + track_width;
hc = h1+h2; // length of the curvy part
side_ext = (target_length - hc)/2;
echo(str("Length of the curvy part (along Y axis): ", hc));
assert(hc<target_length) echo("Curvy part is shorter than the target length - that's OK!");
echo(str("Line-to-line distance: ", l1+l2-track_width));
difference() {
union() {
translate([track_width, 0, 0])
mirror([1, 0, 0])
track_arc(radius = radius, angle = angle,
end1 = "none", end2 = "none",
grooves = true,
both_sides = both_sides,
cutout = cutout,
cutout_corr = cutout_corr,
part_chamfers = true);
translate([-track_width+l1+l2, h1+h2, 0])
rotate([0, 0, 180])
mirror([1, 0, 0])
// Alternative approach - discarded
//translate([-track_width+l1+l2, h1+h2, 0])
//rotate([0, 0, 180])
//mirror([0, 1, 0])
track_arc(radius = radius, angle = angle,
end1 = "none", end2 = "none",
grooves = true,
both_sides = both_sides,
cutout = cutout,
cutout_corr = cutout_corr,
part_chamfers = true);
// Lower extension (close to (0,0))
translate([track_width, -side_ext, 0])
rotate([0, 0, 90])
track(length = side_ext,
cutout = false,
part_chamfers = true,
end1_chamfers = true,
end2_chamfers = false,
grooves = true,
end1 = "none", end2 = "none",
both_sides = both_sides);
// Upper extension (away from (0,0))
translate([l1+l2, h1+h2, 0])
rotate([0, 0, 90])
track(length = side_ext,
cutout = false,
end1_chamfers = false,
end2_chamfers = true,
grooves = true,
end1 = "none", end2 = "none",
both_sides = both_sides);
} //union end
if (end1 == "nest") {
rotate([0, 0, 0])
translate([track_width/2, -side_ext, 0])
rotate([0, 0, 90])
track_nest();
}
if (end2 == "nest") {
translate([l1+l2-track_width/2, h1+h2+side_ext, 0])
rotate([0, 0, -90])
track_nest();
}
}
// Generating plugs
if (end1 == "plug") {
translate([track_width/2, -side_ext, 0])
rotate([0, 0, -90])
track_plug();
}
if (end2 == "plug") {
translate([l1+l2-track_width/2, h1+h2+side_ext, 0])
rotate([0, 0, 90])
track_plug();
}
}
// ### INTERSECTION GENERATOR MODULE
// intersection() module allows to generate various types of intersections.
// Angle between intersecting paths can be defined as well as individual
// lengths of the paths. It is also possible to freely define paths' ending
// types.
// Input parameters:
// - angle - desired angle between two intersecting paths
// - lengthA, lengthB - length of each intersecting path
// - end1A, end2A - desired ends type for the path A ("plug", "nest" or "none")
// - end1B, end2B - desired ends type for the path B ("plug", "nest" or "none")
// - both_sides - controls if the grooves should be generated only on
// the top or also on the bottom:
// -- true - grooves on both sides
// -- false - grooves only on the top
module intersection(angle = 90,
lengthA = 80,
lengthB = 80,
end1A = "nest",
end2A = "plug",
end1B = "nest",
end2B = "plug",
both_sides = true) {
difference() {
union() {
translate([-lengthA/2, -track_width/2, 0])
track(length = lengthA,
cutout = false,
end1 = end1A,
end2 = end2A,
part_chamfers = true,
grooves = false);
rotate([0, 0, angle-90])
translate([track_width/2, -lengthB/2, 0])
rotate([0, 0, 90])
track(length = lengthB,
cutout = false,
end1 = end1B,
end2 = end2B,
part_chamfers = true,
grooves = false);
}
// Grooves on part A
translate([-lengthA/2-cc, -track_width/2, 0])
rotate([90, 0, 90])
linear_extrude(lengthA+2*cc)
grooves(both_sides);
// Grooves on part B
rotate([0, 0, angle-90])
translate([-track_width/2,lengthB/2+cc, 0])
rotate([90, 0, 0])
linear_extrude(lengthB+2*cc)
grooves(both_sides);
}
}
// ### STRAIGHT TRACK GENERATOR MODULE
// This module generates simple, straight track part of desired lengths
// and with desired endings.
// Input parameters:
// - length - desired length
// - cutout - controls if there should be a cutout inside the straight
// track body, this is to help the part to "lose some weight"
// -- true - cutout enabled
// -- false - cutout disabled
// - end1, end2 - desired ends ("plug", "nest" or "none")
// - part_chamfers - controls if the part should have chamfers along the part:
// -- true - chamfers enabled
// -- false - chamfers disabled
// - end1_chamfers, end2_chamfers - controls if the part should have chamfers
// along the part:
// -- true - chamfers enabled
// -- false - chamfers disabled
// - grooves - enables or disables generation of grooves:
// -- true - grooves enabled
// -- false - grooves disabled
// - both_sides - controls if the grooves should be generated only on
// the top or also on the bottom:
// -- true - grooves on both sides
// -- false - grooves only on the top
module track(length = 20,
cutout = true,
end1 = "nest",
end2 = "plug",
part_chamfers = true,
end1_chamfers = true,
end2_chamfers = true,
grooves = true,
both_sides = false) {
b = track_nest_neck_length + track_nest_radius;
cutout_start = (end1 == "nest") ? tr_co_w + b : tr_co_w;
cutout_end = (end2 == "nest") ? tr_co_w + b : tr_co_w;
difference() {
rotate([90, 0, 90])
linear_extrude(length)
track_blueprint(grooves = grooves,
part_chamfers = part_chamfers,
both_sides = both_sides);
if (end1 == "nest") {
install_nest(nest_rotate = 0, length = 0);
}
if (end2 == "nest") {
install_nest(nest_rotate = 180, length = length);
}
if (cutout) {
if (length - cutout_start - cutout_end >= 0) {
hull() {
translate([cutout_start, track_width/2, -cc])
cylinder(d = tr_co_w, h = track_height + 2*cc);
translate([length - cutout_end, track_width/2, -cc])
cylinder(d = tr_co_w, h = track_height + 2*cc);
}
} //if (minimum length)
} //if (cutout)
// Corner chamfers
if (end1_chamfers) {
for (y = [ 0, track_width]) {
translate([0, y, track_height/2])
rotate([0, 0, 45])
cube([track_chamfer,
track_chamfer,
track_height],
center = true);
}
}
if (end2_chamfers) {
for (y = [ 0, track_width]) {
translate([length, y, track_height/2])
rotate([0, 0, 45])
cube([track_chamfer,
track_chamfer,
track_height],
center = true);
}
}
} //main difference
// Generating plugs
if (end1 == "plug") {
install_plug(180, 0);
}
if (end2 == "plug") {
install_plug(0, length);
}
}
// Helping module for track() module - installs a plug
module install_plug(plug_rotate = 180, length = 0) {
translate([length, track_width/2, 0])
rotate([0, 0, plug_rotate])
track_plug();
}
// Helping module for track() module - installs a nest
module install_nest(nest_rotate = 180, length = 0) {
translate([length, track_width/2, 0])
rotate([0, 0, nest_rotate])
track_nest();
translate([length, track_width/2, track_height/2 - cc])
rotate([0, 0, 45])
cube([track_nest_neck_width,
track_nest_neck_width,
track_height + 2*cc],
center = true);
}
// ### BRIDGE GENERATOR MODULE
// Input parameters:
// - what_to_generate - controls what is generated by the module,
// allowed values:
// -- 0 (zero) - shows a bridge overview with calculation of
// the bridge height
// -- 1 - shows ONLY a bridge ground part
// -- 2 - shows ONLY a bridge slope (upper) part
// -- 3 - shows ONLY a bridge pillar part
// - bridge_angle - an angle used to generate "slope" parts of
// the bridge components
// - slope_radius - radius used when generating "slope" parts
// - straight_part_l - straight track length which will be connecting
// bridge ground and upper parts, this is needed in order to
// calculate overall bridge height and to present an overview
// - cutout - used for material saving, it cuts out the middle part
// from a track body if the track path is of sufficient length:
// -- true - the module will try to make a cutout
// -- false - cutouts disabled
// - pillar_l - a pillar length, used for generating a bridge pillar
module generate_bridge(what_to_generate = 0,
bridge_angle = 14,
slope_radius = 100,
straight_part_l = 205,
cutout = true,
pillar_l = 50) {
// Computing all horizontal and vertical offsets
y_off_bg = sin(bridge_angle)*slope_radius + cos(bridge_angle)*slope_straight_plug_length;
y_off_sl = sin(bridge_angle)*slope_radius + cos(bridge_angle)*slope_straight_nest_length;
h_off_bg = slope_radius*(1-cos(bridge_angle)) + sin(bridge_angle) * slope_straight_plug_length;
y_off_straight = cos(bridge_angle) * straight_part_l;
if (what_to_generate == 0 || what_to_generate == 1)
track_bridge_ground(radius = slope_radius,
angle = bridge_angle,
cutout = cutout);
if (what_to_generate == 0)
color("LawnGreen")
translate([track_width, y_off_bg, h_off_bg])
rotate([0, -bridge_angle, 90])
track(straight_part_l,
cutout = cutout);
if (what_to_generate == 0 || what_to_generate == 2)
translate([track_width, y_off_bg + y_off_straight + y_off_sl, 0])
rotate([0, 0, 180])
track_bridge_slope(radius = slope_radius,
angle = bridge_angle,
cutout = cutout,
slope_part_length = straight_part_l);
if (what_to_generate == 0 || what_to_generate == 3)
translate([0, y_off_bg + y_off_straight + y_off_sl + 100, 0])
track_bridge_straight(length = pillar_l,
radius = slope_radius,
angle = bridge_angle,
cutout = cutout,
slope_part_length = straight_part_l);
// Bridge height
sl_h = slope_radius - (cos(bridge_angle) * slope_radius);
sl_str_h = sin(bridge_angle) * (straight_part_l
+ slope_straight_nest_length
+ slope_straight_plug_length);
bridge_height = 2 * sl_h + sl_str_h;
if (what_to_generate == 0)
translate([0, y_off_bg + y_off_straight + y_off_sl + 50, bridge_height/6])
rotate([0, -90, 0])
color("OrangeRed")
text(str(round(bridge_height), " mm"), size = bridge_height/6);
echo("Bridge height (distance between ground and bottom layer of the top bridge track): ", str(bridge_height));
}
// BRIDGE GROUND PART - called by generate_bridge()
// Input parameters:
// - angle - an angle used to generate the "slope" part of
// the bridge ground part
// - radius - radius used when generating the "slope" part
// - cutout - used for material saving, it cuts out the middle part
// from a track body if the track path is of sufficient length:
// -- true - the module will try to make a cutout
// -- false - cutouts disabled
module track_bridge_ground(radius = 200, angle = 20, cutout = true) {
sl_h = radius - (cos(angle) * radius);
sl_dis = sin(angle) * radius;
difference() {
union() {
rotate([0, 90, 0])
translate([-radius, 0, 0]) {
rotate_extrude(angle = angle, convexity = 10)
translate([radius, 0 , 0])
rotate([0, 0, 90])
track_blueprint();
// Low end (nest)
translate([radius, 0, 0])
rotate([90, 0, -90])
track(length = slope_straight_nest_length,
cutout = false,
end1 = "none",
end2 = "nest",
part_chamfers = true,
end1_chamfers = false,
end2_chamfers = true,
grooves = true,
both_sides = false);
// High end (plug)
rotate([0, 0, angle])
translate([radius, slope_straight_plug_length, 0]) {
rotate([90, 0, -90])
track(length = slope_straight_plug_length,
cutout = false,
end1 = "plug",
end2 = "none",
part_chamfers = true,
end1_chamfers = true,
end2_chamfers = false,
grooves = true,
both_sides = false);
translate([0, -slope_straight_plug_length, track_width-track_chamfer])
rotate([0, 90, 0])
cube([track_width - 2 * track_chamfer,
slope_straight_plug_length + track_plug_neck_length - track_plug_radius,
2.5]);
}
}
} //union
if (cutout) {
if (true) { // TO DO: work out a rule
hull() {
translate([track_width/2, tr_co_w, 0])
cylinder(d = tr_co_w, h = 2*sl_h);
translate([track_width/2, sl_dis - tr_co_w, 0])
cylinder(d = tr_co_w, h = 2*sl_h);
}
} //if (minimum length)
} //if (cutout)
} //difference
// Pillar
translate([track_chamfer, sl_dis-slope_straight_plug_length/2, 0]) {
cube([track_width - 2*track_chamfer, bridge_pillar_depth, sl_h]);
}
}
// BRIDGE UPPER SLOPE PART - called by generate_bridge()
// Input parameters:
// - angle - an angle used to generate the "slope" part of
// the bridge upper slope part
// - radius - radius used when generating the "slope" part
// - cutout - used for material saving, it cuts out the middle part
// from a track body if the track path is of sufficient length:
// -- true - the module will try to make a cutout
// -- false - cutouts disabled
// - straight_part_l - straight track length which will be connecting
// bridge ground and upper parts, this is needed in order to
// calculate overall bridge height and to present an overview
module track_bridge_slope(radius = 200, angle = 20, cutout = true, slope_part_length = 146) {
sl_h = radius - (cos(angle) * radius);
sl_dis = sin(angle) * radius;
sl_str_h = sin(angle) * (slope_part_length
+ slope_straight_nest_length
+ slope_straight_plug_length);
sl_pilr_corr_h = sin(angle) * bridge_pillar_depth;
translate([0, 0, sl_str_h + 2 * sl_h])
difference() {
union() {
rotate([0, -90, 0])
translate([-radius, 0, -track_width])
{
rotate_extrude(angle = angle, convexity = 10)
translate([radius, 0 , 0])
rotate([180, 0, 90])
track_blueprint();
// High ending (nest)
translate([radius, 0, track_width])
rotate([-90, 0, -90])
track(slope_straight_nest_length,
cutout = false,
end1 = "none",
end2 = "nest",
part_chamfers = true,
end1_chamfers = false,
end2_chamfers = true,
grooves = true,
both_sides = false);
translate([radius, -slope_straight_nest_length-track_nest_neck_length, track_chamfer])
rotate([0, -90, 0])
cube([track_width - 2 * track_chamfer,
slope_straight_nest_length + track_nest_neck_length,
2.5]);
// Low ending (nest)
rotate([0, 0, angle])
translate([radius, slope_straight_nest_length, track_width]) {
rotate([-90, 0, -90])
track(slope_straight_nest_length,
cutout = false,
end1 = "nest",
end2 = "none",
part_chamfers = true,
end1_chamfers = true,
end2_chamfers = false,
grooves = true,
both_sides = false);
translate([-2.5, -slope_straight_nest_length, -track_chamfer])
rotate([0, 90, 0])
cube([track_width - 2 * track_chamfer,
slope_straight_nest_length + track_nest_neck_length,
2.5]);
}
}
} //union
if (cutout) {
if (true) { //length condition to be added
hull() {
translate([track_width/2, 1.5 * tr_co_w, -sl_h])
cylinder(d = tr_co_w, h = 2*sl_h);
translate([track_width/2, sl_dis - 1.5 * tr_co_w, -sl_h])
cylinder(d = tr_co_w, h = 2*sl_h);
}
} //if (minimum length)
} //if (cutout)
} //difference
// Higher pillar
translate([track_chamfer, -slope_straight_nest_length, 0]) {
cube([track_width - 2*track_chamfer, bridge_pillar_depth, 2 * sl_h + sl_str_h]);
}
// Lower pillar
translate([track_chamfer, sl_dis-bridge_pillar_depth, 0]) {
cube([track_width - 2*track_chamfer, bridge_pillar_depth, sl_h + sl_str_h+sl_pilr_corr_h]);
}
// Pillars connecting bar
translate([track_chamfer, -slope_straight_nest_length, 0])
cube([track_width - 2*track_chamfer, sl_dis+slope_straight_nest_length, 3]);
/*
// Bridge height
echo("Bridge height (distance between ground and bottom layer of the top bridge track): ", str(2 * sl_h + sl_str_h));
*/
}
// BRIDGE PILLAR - called by generate_bridge()
// Input parameters:
// - length - length of the straight pillar track
// - angle - an angle used to generate the "slope" parts of
// the bridge components
// - radius - radius used for generation of the "slope" bridge parts
// - cutout - used for material saving, it cuts out the middle part
// from a track body if the track path is of sufficient length:
// -- true - the module will try to make a cutout
// -- false - cutouts disabled
// - straight_part_l - straight track length which will be connecting
// bridge ground and upper parts, this is needed in order to
// calculate overall bridge height and to present an overview
module track_bridge_straight(length = 50, radius = 200, angle = 20, cutout = true, slope_part_length = 146) {
sl_h = radius - (cos(angle) * radius);
sl_dis = sin(angle) * radius;
sl_str_h = sin(angle) * (slope_part_length
+ slope_straight_nest_length
+ slope_straight_plug_length);
sl_pilr_corr_h = sin(angle) * bridge_pillar_depth;
translate([0, 0, sl_str_h + 2 * sl_h])
rotate([0, 0, 90])
translate([0, -track_width, 0]) {
// Straight track part
track(length = length, cutout = cutout);
// Nest side
translate([track_nest_radius + track_plug_neck_length + track_chamfer, track_chamfer, -2.5])
rotate([0, 0, 90])
cube([track_width - 2 * track_chamfer,
2* track_nest_radius + track_nest_neck_length +
track_chamfer,
2.5]);
// Plug side
translate([length + track_plug_neck_length-track_plug_radius, track_chamfer, -2.5])
rotate([0, 0, 90])
cube([track_width - 2 * track_chamfer,
bridge_pillar_depth +
track_plug_neck_length -
track_plug_radius,
2.5]);
}
// First pillar
translate([track_chamfer, -track_nest_radius, 0]) {
cube([track_width - 2*track_chamfer, bridge_pillar_depth, 2 * sl_h + sl_str_h]);
}
// Second pillar
translate([track_chamfer, length - bridge_pillar_depth, 0]) {
cube([track_width - 2*track_chamfer, bridge_pillar_depth, 2 * sl_h + sl_str_h]);
}
// Pillars connecting bar
translate([track_chamfer, -track_nest_radius, 0])
cube([track_width - 2*track_chamfer, length + track_nest_radius, 3]);
}
// ### DOG-BONE GENERATOR MODULE
// This module generates a special part which connects two adjacent
// nests. The part is named after its shape.
// A dog-bone consists of two connected plugs. The plugs have
// a little bit oversized heads (when compared to standard plugs) and
// they usually give snug fit. You will need to use some force to
// disconnect two parts (nests) connected with use of a dog-bone part.
// Input parameters:
// - track_plug_radius_ext - radius of a desired oversized plug head
module track_dogbone(radius = track_plug_radius_ext) {
cut = 1.25;
shortenby = 0.4;
offset = track_plug_neck_width/2 + cut/2;
difference() {
union() {
translate([-shortenby, 0, 0])
track_plug(radius);
translate([shortenby, 0, 0])
rotate([0, 0, 180])
track_plug(radius);
}
for(i=[offset, -offset]) {
hull() {
translate([track_plug_neck_length+radius/3, i, -cc])cylinder(d=cut, h = track_height+2*cc);
translate([-track_plug_neck_length-+radius/3, i, -cc])cylinder(d=cut, h = track_height+2*cc);
}
}
}
}
// Module track_plug() is used by multiple other modules for generating
// a plug at a desired track ending
module track_plug(radius = track_plug_radius,
neck_w = track_plug_neck_width,
neck_l = track_plug_neck_length){
translate([neck_l, 0, 0])
rotate([0, 0, 180]) {
rotate_extrude(angle = 360) {
polygon([[0, 0],
[radius - track_chamfer/2, 0],
[radius, track_chamfer/2],
[radius, track_height - track_chamfer/2],
[radius - track_chamfer/2, track_height],
[0, track_height]]);
}
translate([0, -neck_w/2, 0])
cube([neck_l, neck_w, track_height]);
}
}
// Module track_nest() is used by multiple other modules for generating
// a nest at a desired track ending
module track_nest(radius = track_nest_radius,
neck_w = track_nest_neck_width,
neck_l = track_nest_neck_length) {
translate([neck_l-cc, 0, 0])
rotate([0, 0, 180]) {
rotate_extrude(angle = 360) {
#polygon([[0, -cc],
[radius + track_chamfer/2, -cc],
[radius, track_chamfer/2],
[radius, track_height - track_chamfer/2],
[radius + track_chamfer/2, track_height + 2*cc],
[0, track_height + 2* cc]]);
}
translate([0, -neck_w/2, -cc])
difference() {
cube([neck_l, neck_w, track_height + 2*cc]);
translate([0, 0, 0]) {
}
}
}
}
// Track shape blueprint
module track_blueprint(grooves = true, both_sides = false, part_chamfers = true) {
difference() {
square([track_width, track_height]);
// Corner champers
if (part_chamfers) {
part_chamfers();
}
// Rail grooves
if (grooves)
if (both_sides) grooves(true);
else grooves(false);
} //difference
}
// Helping module to add chamfers to the track blueprint
module part_chamfers() {
for (x = [ 0, track_width]) {
for (y = [ 0, track_height]) {
translate([x, y, 0])
rotate([0, 0, 45])
square(track_chamfer, center = true);
}
}
}
// Helping module for chamfer shape
module chamfer() {
rotate([0, 0, 45])
square(track_chamfer, center = true);
}
// Helping module used for generation of track grooves
module grooves(both_sides = false) {
for (x = [-track_well_spacing/2, track_well_spacing/2]) {
translate([track_width/2+x, track_height-track_well_depth+cc, 0])
rail_well();
if (both_sides) {
translate([track_width/2+x, track_well_depth-cc, 0])
rotate([0, 0, 180]) rail_well();
}
}
}
// Helping function which defines a grove shape
module rail_well() {
translate([-track_well_width_bottom/2, 0, 0])
polygon([[0, 0],
[track_well_width_bottom, 0],
[track_well_width_bottom+tr_wl_w_stick_out, track_well_depth],
[-tr_wl_w_stick_out, track_well_depth]]);
}
// ### ADAPTER GENERATOR MODULE
// This module generates an adapter between two different track systems.
// It is possible to generate adapters of various length between IKEA
// and BRIO or between one of them and another systems which dimensions
// are defined in the general dimension variables at the beginning
// of this file.
// Input parameters:
// - length - desired length of an adapter
// - nest - nest system, "B" for BRIO, "I" for IKEA, else for a system
// defined in the general dimension variables (track_plug_xxxxxxxx)
// - plug - plug system, "B" for BRIO, "I" for IKEA, else for a system
// defined in the general dimension variables (track_nest_xxxxxxxx)
module tracks_adapter(length = 30, nest = "B", plug = "I") {
txt_size_p = 9;
txt_size_n = 8;
a_nest_radius = (nest == "B") ? brio_track_nest_radius
: (nest == "I")
? ikea_track_nest_radius
: track_nest_radius;
a_nest_neck_w = (nest == "B") ? brio_track_nest_neck_width
: (nest == "I")
? ikea_track_nest_neck_width
: track_nest_neck_width;
a_nest_neck_l = (nest == "B") ? brio_track_nest_neck_length
: (nest == "I")
? ikea_track_nest_neck_length
: track_nest_neck_length;
a_nest_letter = (nest == "B") ? brio_track_nest_txt
: (nest == "I")
? ikea_track_nest_txt
: track_nest_txt;
a_plug_radius = (plug == "B") ? brio_track_plug_radius
: (plug == "I")
? ikea_track_plug_radius
: track_plug_radius;
a_plug_neck_w = (plug == "B") ? brio_track_plug_neck_width
: (plug == "I")
? ikea_track_plug_neck_width
: track_plug_neck_width;
a_plug_neck_l = (plug == "B") ? brio_track_plug_neck_length
: (plug == "I")
? ikea_track_plug_neck_length
: track_plug_neck_length;
a_plug_letter = (plug == "B") ? brio_track_plug_txt
: (plug == "I")
? ikea_track_plug_txt
: track_plug_txt;
difference() {
rotate([90, 0, 90])
linear_extrude(length)
track_blueprint(grooves = true,
part_chamfers = true,
both_sides = false);
// Generating Nest
translate([0, track_width/2, 0])
track_nest(radius = a_nest_radius,
neck_w = a_nest_neck_w,
neck_l = a_nest_neck_l);
translate([0, track_width/2, track_height/2 - cc])
rotate([0, 0, 45])
cube([a_nest_neck_w,
a_nest_neck_w,
track_height + 2*cc],
center = true);
// Corner chamfers
for (y = [ 0, track_width]) {
translate([0, y, track_height/2])
rotate([0, 0, 45])
cube([track_chamfer,
track_chamfer,
track_height],
center = true);
}
for (y = [ 0, track_width]) {
translate([length, y, track_height/2])
rotate([0, 0, 45])
cube([track_chamfer,
track_chamfer,
track_height],
center = true);
}
// Nest letter
translate([a_nest_neck_l+a_nest_radius+2,
track_width/2-a_nest_neck_w/2-txt_size_n/1.7,
track_height-1.4]) {
linear_extrude(1.5) text(a_nest_letter,
font = "Courier New:style=Bold",
size = txt_size_n);
}
} //main difference
// Generating Plug
difference() {
translate([length, track_width/2, 0])
track_plug(radius = a_plug_radius,
neck_w = a_plug_neck_w,
neck_l = a_plug_neck_l);
// Plug letter
translate([length + a_plug_neck_l-txt_size_p/2+1,
track_width/2-txt_size_p/2+0.75,
track_height-1.4]) {
linear_extrude(1.5) text(a_plug_letter,
font = "Courier New:style=Bold",
size = txt_size_p);
}
}
}