diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c804a9..af89032 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,21 @@
This changelog is generated by `changelog.py` using manually added semantic version tags to classify commits as breaking changes, additions or fixes.
-* 2021-03-13 [`0d160da`](https://github.com/nophead/NopSCADlib/commit/0d160da40c5d06c5d088733b488d726d1c4d98b0 "show commit") [C.P.](# "Chris Palmer") changelog now omits "Updated changelog" commits.
+* 2021-03-14 [`d2c795f`](https://github.com/nophead/NopSCADlib/commit/d2c795f5f5c564ec4686a7857bd894738a93a6a0 "show commit") [S.](# "SmoothieAq") fix nan angle (hopefully)
+
+* 2021-03-14 [`573c507`](https://github.com/nophead/NopSCADlib/commit/573c50774bfb2edae25a415ca864abb39c4c1bcf "show commit") [S.](# "SmoothieAq") changes after review
+
+* 2021-03-11 [`2403347`](https://github.com/nophead/NopSCADlib/commit/240334784db8002468fe0f51c1f6404a64fe44a4 "show commit") [S.](# "SmoothieAq") Extension to `belt.scad`
+
+ * Can now:
+- render open loops
+- twist the belt
+- use pulleys instead of radius in the points list
+
+ * Fixes some precision a few places
+Breaking change in `belt_length();` now requires a type argument
+
+* 2021-03-13 [`4b93623`](https://github.com/nophead/NopSCADlib/commit/4b93623492a2b8a073ab9dccc91fb2b21c475f21 "show commit") [C.P.](# "Chris Palmer") `changelog.py` now omits "Updated changelog" commits.
* 2021-03-13 [`544e69c`](https://github.com/nophead/NopSCADlib/commit/544e69c71b404636df320be278b3fa7442f10429 "show commit") [C.P.](# "Chris Palmer") `pulley_pr()` now has an optional belt type for non-standard belt over smooth pulleys.
diff --git a/libtest.png b/libtest.png
index b235a5e..e538c40 100644
Binary files a/libtest.png and b/libtest.png differ
diff --git a/readme.md b/readme.md
index f636db2..8a7e73b 100644
--- a/readme.md
+++ b/readme.md
@@ -267,13 +267,22 @@ SCSnUU and SCSnLUU bearing blocks
---
## Belts
-Models timing belt running over toothed or smooth pulleys and calculates an accurate length.
-Only models 2D paths, so not crossed belt core XY!
+Models timing belt running in a path over toothed or smooth pulleys and calculates an accurate length.
+Only models 2D paths, belt may twist to support crossed belt core XY and other designes where the belt twists!
+
+By default the path is a closed loop. An open loop can be specified by specifying `open=true`, and in that case the start and end points are not connected, leaving the loop open.
+
+To get a 180 degree twist of the loop, you can use the `twist` argument. `Twist` can be a single number, and in that case the belt will twist after
+the position with that number. Alternatively `twist` can be a list of boolean values with a boolean for each position; the belt will then twist after
+the position that have a `true` value in the `twist` list. If the path is specified with pulley/idler types, then you can use `auto_twist=true`; in
+that case the belt will automatically twist so the back of the belt always runs against idlers and the tooth side runs against pullies. If you use
+`open=true` then you might also use `start_twist=true` to let the belt start the part with the back side out.
+
+The path must be specified as a list of positions. Each position should be either a vector with `[x, y, pulley]` or `[x, y, r]`. A pully is a type from
+`pulleys.scad`, and correct radius and angle will automatically be calculated. Alternatively a radius can be specified directly.
To make the back of the belt run against a smooth pulley on the outside of the loop specify a negative pitch radius.
-
-By default the path is a closed loop but a gap length and position can be specified to make open loops.
-To draw the gap its XY position is specified by `gap_pos`. `gap_pos.z` can be used to specify a rotation if the gap is not at the bottom of the loop.
+Alternativley you can just specify smooth pulleys in the path, and it will then happen automatically.
Individual teeth are not drawn, instead they are represented by a lighter colour.
@@ -295,13 +304,15 @@ Individual teeth are not drawn, instead they are represented by a lighter colour
### Functions
| Function | Description |
|:--- |:--- |
-| `belt_length(points, gap = 0)` | Compute belt length given path and optional gap |
+| `_belt_points_info(type, points, open, twist, auto_twist, start_twist)` | Helper function that calculates [twist, istwisted, points, tangents, arcs] |
+| `belt_length(type, points, open = false)` | Compute belt length given path |
| `belt_pitch_to_back(type)` | Offset of the back from the pitch radius |
+| `belt_pulley_pr(type, pulley, twisted=false)` | Pitch radius. Default it expects the belt tooth to be against a toothed pulley an the backside to be against a smooth pulley (an idler). If `twisted` is true, the the belt is the other way around. |
### Modules
| Module | Description |
|:--- |:--- |
-| `belt(type, points, gap = 0, gap_pos = undef, belt_colour = grey(20)` | Draw a belt path given a set of points and pitch radii where the pulleys are. Closed loop unless a gap is specified |
+| `belt(type, points, belt_colour = grey(20)` | Draw a belt path given a set of points and pitch radii where the pulleys are. Closed loop unless open is specified |

@@ -309,17 +320,18 @@ Individual teeth are not drawn, instead they are represented by a lighter colour
| Qty | Module call | BOM entry |
| ---:|:--- |:---|
| 1 | `belt(GT2x6, [ ... ])` | Belt GT2 x 6mm x 128mm |
-| 2 | `belt(GT2x6, [ ... ], 80, [0, 0])` | Belt GT2 x 6mm x 572mm |
+| 1 | `belt(GT2x6, [ ... ])` | Belt GT2 x 6mm x 552mm |
+| 2 | `belt(GT2x6, [ ... ])` | Belt GT2 x 6mm x 556mm |
| 1 | `belt(T2p5x6, [ ... ])` | Belt T2.5 x 6mm x 130mm |
| 1 | `belt(T5x10, [ ... ])` | Belt T5 x 10mm x 130mm |
| 1 | `belt(T5x6, [ ... ])` | Belt T5 x 6mm x 130mm |
| 2 | `insert(F1BM3)` | Heatfit insert M3 |
| 2 | `pulley(GT2x16_toothed_idler)` | Pulley GT2 idler 16 teeth |
| 4 | `pulley(GT2x20_toothed_idler)` | Pulley GT2 idler 20 teeth |
-| 2 | `pulley(GT2x16_plain_idler)` | Pulley GT2 idler smooth 9.63mm |
-| 2 | `pulley(GT2x20ob_pulley)` | Pulley GT2OB 20 teeth |
+| 6 | `pulley(GT2x16_plain_idler)` | Pulley GT2 idler smooth 9.63mm |
+| 3 | `pulley(GT2x20ob_pulley)` | Pulley GT2OB 20 teeth |
| 2 | `screw(M3_cs_cap_screw, 20)` | Screw M3 cs cap x 20mm |
-| 4 | `screw(M3_grub_screw, 6)` | Screw M3 grub x 6mm |
+| 6 | `screw(M3_grub_screw, 6)` | Screw M3 grub x 6mm |
Top
@@ -5628,8 +5640,8 @@ allows flexible positioning of the motors.
### Vitamins
| Qty | Module call | BOM entry |
| ---:|:--- |:---|
-| 1 | `belt(GT2x6, [ ... ], [10.0078, 11.69], [0, -24.686])` | Belt GT2 x 6mm x 742mm |
-| 1 | `belt(GT2x6, [ ... ], [10.0078, 11.69], [0, -24.686])` | Belt GT2 x 6mm x 852mm |
+| 1 | `belt(GT2x6, [ ... ])` | Belt GT2 x 6mm x 728mm |
+| 1 | `belt(GT2x6, [ ... ])` | Belt GT2 x 6mm x 824mm |
| 7 | `pulley(GT2x16_toothed_idler)` | Pulley GT2 idler 16 teeth |
| 3 | `pulley(GT2x16_plain_idler)` | Pulley GT2 idler smooth 9.63mm |
| 2 | `pulley(GT2x20ob_pulley)` | Pulley GT2OB 20 teeth |
@@ -5824,8 +5836,11 @@ Maths utilities for manipulating vectors and matrices.
| `euler(R)` | Convert a rotation matrix to a Euler rotation vector. |
| `identity(n, x = 1)` | Construct an arbitrary size identity matrix |
| `invert(m)` | Invert a matrix |
+| `map(v, func)` | make a new vector where the func function argument is applied to each element of the vector v |
+| `mapi(v, func)` | make a new vector where the func function argument is applied to each element of the vector v. The func will get the index number as first argument, and the element as second argument. |
| `nearly_zero(x)` | True if x is close to zero |
| `radians(degrees)` | Convert radians to degrees |
+| `reduce(v, func, unity)` | reduce a vector v to a single entity by applying the func function recursively to the reduced value so far and the next element, starting with unity as the initial reduced value |
| `reverse(v)` | Reverse a vector |
| `rot2_z(a)` | Generate a 2x2 matrix to rotate around z |
| `rot3_z(a)` | Generate a 3x3 matrix to rotate around z |
@@ -5836,12 +5851,14 @@ Maths utilities for manipulating vectors and matrices.
| `solve(m, i = 0, j = 0)` | Solve each row ensuring diagonal is not zero |
| `solve_row(m, i)` | Make diagonal one by dividing the row by it and subtract from other rows to make column zero |
| `sqr(x)` | Square x |
+| `sumv(v)` | sum a vector of values that can be added with "+" |
| `tanh(x)` | hyperbolic tangent |
| `transform(v, m)` | Apply 4x4 transform to a 3 vector by extending it and cropping it again |
| `transform_points(path, m)` | Apply transform to a path |
| `translate(v)` | Generate a 4x4 translation matrix, `v` can be `[x, y]`, `[x, y, z]` or `z` |
| `transpose(m)` | Transpose an arbitrary size matrix |
| `unit(v)` | Convert `v` to a unit vector |
+| `vec2(v)` | Return a 2 vector with the first two elements of `v` |
| `vec3(v)` | Return a 3 vector with the first three elements of `v` |
| `vec4(v)` | Return a 4 vector with the first three elements of `v` |
@@ -5957,8 +5974,9 @@ Because the tangents need to be calculated to find the length these can be calcu
| Function | Description |
|:--- |:--- |
| `circle_tangent(p1, p2)` | Compute the clockwise tangent between two circles represented as [x,y,r] |
+| `rounded_polygon_arcs(points, tangents)` | Compute the arcs at the points, for each point [angle, rotate_angle, length] |
| `rounded_polygon_length(points, tangents)` | Calculate the length given the point list and the list of tangents computed by ` rounded_polygon_tangents` |
-| `rounded_polygon_tangents(points)` | Compute the straight sections needed to draw and to compute the lengths |
+| `rounded_polygon_tangents(points)` | Compute the straight sections between a point and the next point, for each section [start_point, end_point, length] |
### Modules
| Module | Description |
diff --git a/tests/belts.scad b/tests/belts.scad
index 9894386..cc47183 100644
--- a/tests/belts.scad
+++ b/tests/belts.scad
@@ -25,9 +25,9 @@ use <../utils/layout.scad>
module belt_test() {
p2 = [-75, -50];
p3 = [-75, 100];
- p4 = [75, 100];
+ p4 = [ 75, 100];
- p5 = [75 + pulley_pr(GT2x20ob_pulley) - pulley_pr(GT2x16_plain_idler), +pulley_pr(GT2x16_plain_idler)];
+ p5 = [ 75 + pulley_pr(GT2x20ob_pulley) - pulley_pr(GT2x16_plain_idler), +pulley_pr(GT2x16_plain_idler)];
p6 = [-75 + pulley_pr(GT2x20ob_pulley) + pulley_pr(GT2x16_plain_idler), -pulley_pr(GT2x16_plain_idler)];
module pulleys(flip = false) {
@@ -52,19 +52,21 @@ module belt_test() {
translate(p6) pulley_assembly(GT2x16_plain_idler);
}
- path = [ [p5.x, p5.y, pulley_pr(GT2x16_plain_idler)],
+ path = [ [-40, 0, 0],
[p6.x, p6.y, -pulley_pr(GT2x16_plain_idler)],
[p2.x, p2.y, pulley_pr(GT2x20ob_pulley)],
[p3.x, p3.y, pulley_pr(GT2x20ob_pulley)],
- [p4.x, p4.y, pulley_pr(GT2x20ob_pulley)]
+ [p4.x, p4.y, pulley_pr(GT2x20ob_pulley)],
+ [p5.x, p5.y, pulley_pr(GT2x16_plain_idler)],
+ [40, 0, 0],
];
belt = GT2x6;
- belt(belt, path, 80, [0, 0]);
+ belt(belt, path, open = true);
pulleys();
translate_z(20)
hflip() {
- belt(belt, path, 80, [0, 0], belt_colour = grey(90), tooth_colour = grey(50));
+ belt(belt, path, open = true, belt_colour = grey(90), tooth_colour = grey(50));
pulleys(flip=true);
}
@@ -72,6 +74,31 @@ module belt_test() {
layout([for(b = belts) belt_width(b)], 10)
rotate([0, 90, 0])
belt(belts[$i], [[0, 0, 20], [0, 1, 20]], belt_colour = $i%2==0 ? grey(90) : grey(20), tooth_colour = $i%2==0 ? grey(70) : grey(50));
+
+ // new example with open loop - this is a simplified example of the style used for example for the BLV 3D printer
+ pulley = GT2x20ob_pulley;
+ idler = GT2x16_plain_idler;
+ corners = [[-75,-50],[75,100]];
+ carriagepos = [0,0];
+ carriagew = 80;
+
+ points = [
+ [carriagepos.x - carriagew / 2, carriagepos.y, 0],
+ [corners[0].x + belt_pulley_pr(belt, pulley) + belt_pulley_pr(belt, idler), carriagepos.y - belt_pulley_pr(belt, idler), idler],
+ [corners[0].x, corners[0].y, pulley],
+ [corners[0].x, corners[1].y, idler],
+ [corners[1].x, corners[1].y, idler],
+ [corners[1].x, carriagepos.y + belt_pulley_pr(belt, idler), idler],
+ [carriagepos.x + carriagew / 2, carriagepos.y, 0]
+ ];
+ translate_z(-30) {
+ belt(belt, points, open=true, auto_twist=true);
+ for (p = points)
+ if (is_list(p.z))
+ translate([p.x, p.y, 0])
+ pulley_assembly(p.z);
+ }
+
}
if($preview)
diff --git a/tests/png/belts.png b/tests/png/belts.png
index b9bc543..8ffe2a4 100644
Binary files a/tests/png/belts.png and b/tests/png/belts.png differ
diff --git a/tests/png/core_xy.png b/tests/png/core_xy.png
index 17dd067..6bf6ba7 100644
Binary files a/tests/png/core_xy.png and b/tests/png/core_xy.png differ
diff --git a/tests/png/rounded_polygon.png b/tests/png/rounded_polygon.png
index db78c0d..1042cad 100644
Binary files a/tests/png/rounded_polygon.png and b/tests/png/rounded_polygon.png differ
diff --git a/utils/core_xy.scad b/utils/core_xy.scad
index 0ae3299..5cdb412 100644
--- a/utils/core_xy.scad
+++ b/utils/core_xy.scad
@@ -51,15 +51,20 @@ function coreXY_lower_tooth_colour(type) = type[8]; //! Colour of the lower b
// relative to the anchor pulley so that the belts align properly
function coreXY_drive_pulley_x_alignment(type) = //! Belt alignment offset of the drive pulley relative to the anchor pulley
(pulley_od(coreXY_drive_pulley(type)) - pulley_od(coreXY_toothed_idler(type))) / 2;
+
function coreXY_coincident_separation(type) = //! Value of x, y separation to make y-carriage pulleys coincident
[ -coreXY_plain_idler_offset(type).x, -(pulley_od(coreXY_plain_idler(type)) + pulley_od(coreXY_toothed_idler(type)))/2, 0 ];
+
function coreXY_plain_idler_offset(type) = //! Offset of y-carriage plain idler
[ (pulley_od(coreXY_plain_idler(type)) + pulley_od(coreXY_drive_pulley(type))) / 2 + coreXY_drive_pulley_x_alignment(type), pulley_od(coreXY_plain_idler(type))/2, 0 ];
+
function coreXY_toothed_idler_offset(type) = //! offset of y-carriage toothed idler
[ 0, -pulley_pr(coreXY_toothed_idler(type)), 0 ];
+
// helper functions for positioning idlers when the stepper motor drive pulley is offset
function coreXY_drive_toothed_idler_offset(type) = //! Offset of toothed drive idler pulley
[ 0, coreXY_drive_pulley_x_alignment(type), 0 ];
+
function coreXY_drive_plain_idler_offset(type) = //! Offset of plain drive idler pulley
[ coreXY_plain_idler_offset(type).x, -(pulley_od(coreXY_plain_idler(type)) + pulley_od(coreXY_drive_pulley(type))) / 2, 0 ];
@@ -86,7 +91,7 @@ module coreXY_half(type, size, pos, separation_y = 0, x_gap = 0, plain_idler_off
// toothed idler for offset stepper motor drive pulley
p3t_type = coreXY_toothed_idler(type);
- p3t = [ -size.x / 2 + (drive_pulley_offset.x > 0 ? 0 : 2*coreXY_drive_pulley_x_alignment(type)),
+ p3t = [ -size.x / 2 + (drive_pulley_offset.x > 0 ? 0 : 2 * coreXY_drive_pulley_x_alignment(type)),
size.y / 2 + coreXY_drive_pulley_x_alignment(type) + drive_pulley_offset.y
];
@@ -102,11 +107,11 @@ module coreXY_half(type, size, pos, separation_y = 0, x_gap = 0, plain_idler_off
size.y / 2 - pulley_od(p3p_type) / 2 - pulley_od(p3d_type) / 2 + drive_pulley_offset.y
];
- // dummy pulleys for y separation
- p5_type = p4_type;
- p5 = [ pos.x - size.x / 2, -size.y / 2 + pos.y + separation_y / 2 ];
- p6_type = p0_type;
- p6 = [ pos.x - size.x / 2, -size.y / 2 + pos.y - separation_y / 2 ];
+ // Start and end points
+ start_p = [ pos.x - size.x / 2 + x_gap / 2, -size.y / 2 + pos.y - separation_y / 2, 0 ];
+ end_p = [ pos.x - size.x / 2 - x_gap / 2, -size.y / 2 + pos.y + separation_y / 2, 0 ];
+
+ //p6_type = p0_type;
module show_pulleys(show_pulleys) {// Allows the pulley colour to be set for debugging
if (is_list(show_pulleys))
@@ -119,16 +124,21 @@ module coreXY_half(type, size, pos, separation_y = 0, x_gap = 0, plain_idler_off
show_pulleys(show_pulleys) {
translate(p0)
pulley_assembly(p0_type); // y-carriage toothed pulley
+
translate(p1)
pulley_assembly(p1_type); // bottom right toothed idler pulley
+
translate(p2)
pulley_assembly(p2_type); // bottom left anchor toothed idler pulley
+
translate(p3d)
hflip(hflip)
pulley_assembly(p3d_type); // top left stepper motor drive pulley
+
if (drive_pulley_offset.x) { // idler pulleys for offset stepper motor drive pulley
translate(p3t)
pulley_assembly(p3t_type); // toothed idler
+
translate(p3p)
pulley_assembly(p3p_type); // plain idler
}
@@ -157,20 +167,15 @@ module coreXY_half(type, size, pos, separation_y = 0, x_gap = 0, plain_idler_off
[ p3t.x, p3t.y, pulley_od(p3t_type) / 2 ],
[ p4.x, p4.y, -pulley_od(p4_type) / 2 ]
];
- path1 = [ // use eps for corner radius to get sharp corners so this part of the belt is deleted by the gap
- [ p5.x, p5.y, eps ],
- [ p6.x, p6.y, eps ]
- ];
belt = coreXY_belt(type);
path0 = drive_pulley_offset.x == 0 ? concat(path0a, path0b) : drive_pulley_offset.x > 0 ? concat(path0a, path0c) : concat(path0a, path0d);
- path = separation_y == 0 ? path0 : concat(path0, path1);
+ path = concat([start_p], path0, [end_p]);
belt(type = belt,
points = path,
- gap = [ x_gap + eps, abs(separation_y) + 2 ],
- gap_pos = [ pos.x - size.x / 2, pos.y - size.y / 2 + belt_pitch_height(belt) - belt_thickness(belt) / 2 ],
+ open = true,
belt_colour = lower_belt ? coreXY_lower_belt_colour(type) : coreXY_upper_belt_colour(type),
tooth_colour = lower_belt ? coreXY_lower_tooth_colour(type) : coreXY_upper_tooth_colour(type));
}
@@ -181,6 +186,7 @@ module coreXY(type, size, pos, separation, x_gap, plain_idler_offset = 0, upper_
hflip()
explode(25)
coreXY_half(type, size, [size.x - pos.x - separation.x, pos.y], separation.y, x_gap, plain_idler_offset, [-lower_drive_pulley_offset.x, lower_drive_pulley_offset.y], show_pulleys, lower_belt = true, hflip = true);
+
// upper belt
translate([separation.x, 0, separation.z])
explode(25)
diff --git a/utils/maths.scad b/utils/maths.scad
index 9683e50..1c46aec 100644
--- a/utils/maths.scad
+++ b/utils/maths.scad
@@ -88,6 +88,7 @@ function scale(v) = let(s = is_list(v) ? v : [v, v, v]) //! Generate a 4x4 matr
[0, 0, 0, 1]
];
+function vec2(v) = [v.x, v.y]; //! Return a 2 vector with the first two elements of `v`
function vec3(v) = [v.x, v.y, v.z]; //! Return a 3 vector with the first three elements of `v`
function vec4(v) = [v.x, v.y, v.z, 1]; //! Return a 4 vector with the first three elements of `v`
function transform(v, m) = vec3(m * [v.x, v.y, v.z, 1]); //! Apply 4x4 transform to a 3 vector by extending it and cropping it again
@@ -153,3 +154,10 @@ function circle_intersect(c1, r1, c2, r2) = //! Calculate one point where tw
d = norm(v), // Distance between centres
a = atan2(v.z, v.x) - acos((sqr(d) + sqr(r2) - sqr(r1)) / (2 * d * r2)) // Cosine rule to find angle from c2
) c2 + r2 * [cos(a), 0, sin(a)]; // Point on second circle
+
+function map(v, func) = [ for (e = v) func(e) ]; //! make a new vector where the func function argument is applied to each element of the vector v
+function mapi(v, func) = [ for (i = [0:len(v)-1]) func(i,v[i]) ]; //! make a new vector where the func function argument is applied to each element of the vector v. The func will get the index number as first argument, and the element as second argument.
+function reduce(v, func, unity) = let ( r = function(i,val) i == len(v) ? val : r(i + 1, func(val, v[i])) ) r(0, unity); //! reduce a vector v to a single entity by applying the func function recursively to the reduced value so far and the next element, starting with unity as the initial reduced value
+function sumv(v) = reduce(v, function(a, b) a + b, 0); //! sum a vector of values that can be added with "+"
+
+function xor(a,b) = (a && !b) || (!a && b);
diff --git a/utils/rounded_polygon.scad b/utils/rounded_polygon.scad
index 85d0f6b..dd4b756 100644
--- a/utils/rounded_polygon.scad
+++ b/utils/rounded_polygon.scad
@@ -24,6 +24,7 @@
//! Because the tangents need to be calculated to find the length these can be calculated separately and re-used when drawing to save calculating them twice.
//
include <../utils/core/core.scad>
+use <../utils/maths.scad>
function circle_tangent(p1, p2) = //! Compute the clockwise tangent between two circles represented as [x,y,r]
let(
@@ -36,36 +37,45 @@ function circle_tangent(p1, p2) = //! Compute the clockwise tangent between two
v = [cos(theta), sin(theta)]
)[ p1 + r1 * v, p2 + r2 * v ];
-function rounded_polygon_tangents(points) = //! Compute the straight sections needed to draw and to compute the lengths
- let(len = len(points))
- [for(i = [0 : len - 1])
- let(ends = circle_tangent(points[i], points[(i + 1) % len]))
- for(end = [0, 1])
- ends[end]];
+function rounded_polygon_arcs(points, tangents) = //! Compute the arcs at the points, for each point [angle, rotate_angle, length]
+ let(
+ len = len(points)
+ ) [ for (i = [0: len-1])
+ let(
+ p1 = vec2(tangents[(i - 1 + len) % len][1]),
+ p2 = vec2(tangents[i][0]),
+ p = vec2(points[i]),
+ v1 = p1 - p,
+ v2 = p2 - p,
+ sr = points[i][2],
+ r = abs(sr),
+ a = r < 0.001 ? 0 : let( aa = acos((v1 * v2) / sqr(r)) ) cross(v1, v2) * sign(sr) <= 0 ? aa : 360 - aa,
+ l = PI * a * r / 180,
+ v0 = [r, 0],
+ v = let (
+ vv = norm(v0 - v2) < 0.001 ? 0 : abs(v2.y) < 0.001 ? 180 :
+ let( aa = acos((v0 * v2) / sqr(r)) ) cross(v0, v2) * sign(sr) <= 0 ? aa : 360 - aa
+ ) sr > 0 ? 360 - vv : vv - a
+ ) [a, v, l]
+ ];
-function sumv(v, i = 0, sum = 0) = i == len(v) ? sum : sumv(v, i + 1, sum + v[i]);
+function rounded_polygon_tangents(points) = //! Compute the straight sections between a point and the next point, for each section [start_point, end_point, length]
+ let(len = len(points))
+ [ for(i = [0 : len - 1])
+ let(ends = circle_tangent(points[i], points[(i + 1) % len]))
+ [ends[0], ends[1], norm(ends[0] - ends[1])]
+ ];
// the cross product of 2D vectors is the area of the parallelogram between them. We use the sign of this to decide if the angle is bigger than 180.
function rounded_polygon_length(points, tangents) = //! Calculate the length given the point list and the list of tangents computed by ` rounded_polygon_tangents`
let(
- len = len(points),
- indices = [0 : len - 1],
- straights = [for(i = indices) norm(tangents[2 * i] - tangents[2 * i + 1])],
- arcs = [for(i = indices) let(p1 = tangents[2 * i + 1],
- p2 = tangents[(2 * i + 2) % (2 * len)],
- corner = points[(i + 1) % len],
- c = [corner.x, corner.y],
- v1 = p1 - c,
- v2 = p2 - c,
- r = abs(corner.z),
- a = acos((v1 * v2) / sqr(r))) r ? PI * (cross(v1, v2) <= 0 ? a : 360 - a) * r / 180 : 0]
- )
- sumv(concat(straights, arcs));
+ arcs = rounded_polygon_arcs(points, tangents)
+ ) sumv( map( concat(tangents, arcs), function(e) e[2] ) );
module rounded_polygon(points, _tangents = undef) { //! Draw the rounded polygon from the point list, can pass the tangent list to save it being calculated
len = len(points);
indices = [0 : len - 1];
- tangents = _tangents ? _tangents : rounded_polygon_tangents(points);
+ tangents = [ for (t = _tangents ? _tangents : rounded_polygon_tangents(points)) each [t.x, t.y] ];
difference(convexity = points) {
union() {
@@ -74,6 +84,7 @@ module rounded_polygon(points, _tangents = undef) { //! Draw the rounded polygon
hull() {
translate([points[i].x, points[i].y])
circle(points[i][2]);
+
polygon([tangents[(2 * i - 1 + 2 * len) % (2 * len)], tangents[2 * i], [points[i].x, points[i].y]]);
}
@@ -85,7 +96,7 @@ module rounded_polygon(points, _tangents = undef) { //! Draw the rounded polygon
translate([points[i].x, points[i].y])
circle(-points[i][2]);
- polygon([tangents[(2 * i - 1 + 2 * len) % (2 *len)], tangents[2 * i], [points[i].x, points[i].y]]);
+ polygon([tangents[(2 * i - 1 + 2 * len) % (2 * len)], tangents[2 * i], [points[i].x, points[i].y]]);
}
}
}
diff --git a/vitamins/belt.scad b/vitamins/belt.scad
index 90a9f67..b076046 100644
--- a/vitamins/belt.scad
+++ b/vitamins/belt.scad
@@ -18,19 +18,29 @@
//
//
-//! Models timing belt running over toothed or smooth pulleys and calculates an accurate length.
-//! Only models 2D paths, so not crossed belt core XY!
+//! Models timing belt running in a path over toothed or smooth pulleys and calculates an accurate length.
+//! Only models 2D paths, belt may twist to support crossed belt core XY and other designes where the belt twists!
+//!
+//! By default the path is a closed loop. An open loop can be specified by specifying `open=true`, and in that case the start and end points are not connected, leaving the loop open.
+//!
+//! To get a 180 degree twist of the loop, you can use the `twist` argument. `Twist` can be a single number, and in that case the belt will twist after
+//! the position with that number. Alternatively `twist` can be a list of boolean values with a boolean for each position; the belt will then twist after
+//! the position that have a `true` value in the `twist` list. If the path is specified with pulley/idler types, then you can use `auto_twist=true`; in
+//! that case the belt will automatically twist so the back of the belt always runs against idlers and the tooth side runs against pullies. If you use
+//! `open=true` then you might also use `start_twist=true` to let the belt start the part with the back side out.
+//!
+//! The path must be specified as a list of positions. Each position should be either a vector with `[x, y, pulley]` or `[x, y, r]`. A pully is a type from
+//! `pulleys.scad`, and correct radius and angle will automatically be calculated. Alternatively a radius can be specified directly.
//!
//! To make the back of the belt run against a smooth pulley on the outside of the loop specify a negative pitch radius.
-//!
-//! By default the path is a closed loop but a gap length and position can be specified to make open loops.
-//! To draw the gap its XY position is specified by `gap_pos`. `gap_pos.z` can be used to specify a rotation if the gap is not at the bottom of the loop.
+//! Alternativley you can just specify smooth pulleys in the path, and it will then happen automatically.
//!
//! Individual teeth are not drawn, instead they are represented by a lighter colour.
//
include <../utils/core/core.scad>
use <../utils/rounded_polygon.scad>
use <../utils/maths.scad>
+use
function belt_pitch(type) = type[1]; //! Pitch in mm
function belt_width(type) = type[2]; //! Width in mm
@@ -41,47 +51,102 @@ function belt_pitch_height(type) = type[5] + belt_tooth_height(type); //! Offset
function belt_pitch_to_back(type) = belt_thickness(type) - belt_pitch_height(type); //! Offset of the back from the pitch radius
//
// We model the belt path at the pitch radius of the pulleys and the pitch line of the belt to get an accurate length.
-// The belt is then drawn by offseting each side from the pitch line.
//
-module belt(type, points, gap = 0, gap_pos = undef, belt_colour = grey(20), tooth_colour = grey(50)) { //! Draw a belt path given a set of points and pitch radii where the pulleys are. Closed loop unless a gap is specified
+module belt(type, points, belt_colour = grey(20), tooth_colour = grey(50), open = false, twist = undef, auto_twist = false, start_twist = false) { //! Draw a belt path given a set of points and pitch radii where the pulleys are. Closed loop unless open is specified
width = belt_width(type);
pitch = belt_pitch(type);
thickness = belt_thickness(type);
+
+ info = _belt_points_info(type, points, open, twist, auto_twist, start_twist);
+ dotwist = info[0]; // array of booleans, true if a twist happen after the position
+ twisted = info[1]; // array of booleans, true if the belt is twisted at the position
+ pointsx = info[2]; // array of [x,y,r], r is negative if left-angle (points may have pulleys as third element, but pointsx have radi)
+ tangents = info[3];
+ arcs = info[4];
+ length = ceil(_belt_length(info, open) / pitch) * pitch;
+
part = str(type[0],pitch);
- vitamin(str("belt(", no_point(part), "x", width, ", ", points, arg(gap, 0), arg(gap_pos, undef), "): Belt ", part," x ", width, "mm x ", length, "mm"));
+ vitamin(str("belt(", no_point(part), "x", width, ", ", pointsx, "): Belt ", part," x ", width, "mm x ", length, "mm"));
len = len(points);
- tangents = rounded_polygon_tangents(points);
-
- length = ceil((rounded_polygon_length(points, tangents) - (is_list(gap) ? gap.x + gap.y : gap)) / pitch) * pitch;
-
- module shape() rounded_polygon(points, tangents);
-
- ph = belt_pitch_height(type);
th = belt_tooth_height(type);
- module gap()
- if(gap)
- translate([gap_pos.x, gap_pos.y])
- rotate(is_undef(gap_pos.z) ? 0 : gap_pos.z)
- translate([0, ph - thickness / 2])
- square(is_list(gap) ? [gap.x, gap.y + thickness + eps] : [gap, thickness + eps], center = true);
+ ph = belt_pitch_height(type);
+ module beltp() translate([ph - th, -width / 2]) square([th, width]);
+ module beltb() translate([ph - thickness, -width / 2]) square([thickness - th, width]);
- color(belt_colour)
- linear_extrude(width, center = true)
- difference() {
- offset(-ph + thickness ) shape();
- offset(-ph + th) shape();
- gap();
- }
+ for (i = [0 : len - (open ? 2 : 1)]) {
+ p1 = tangents[i].x;
+ p2 = tangents[i].y;
+ v = p2-p1;
+ a = atan(v.y / v.x) - (v.x < 0 ? 180 : 0); //a2(p2-p1);
+ l = norm(v);
+ translate(p1) rotate([-90, 0, a - 90]) {
+ twist = dotwist[i] ? 180 : 0;
+ mirrored = twisted[i] ? 1 : 0;
+ color(tooth_colour) linear_extrude(l, twist = twist) mirror([mirrored, 0, 0]) beltp();
+ color(belt_colour) linear_extrude(l, twist = twist) mirror([mirrored, 0, 0]) beltb();
+ }
+ }
- color(tooth_colour)
- linear_extrude(width, center = true)
- difference() {
- offset(-ph + th) shape();
- offset(-ph) shape();
- gap();
- }
+ for (i = [(open ? 1 : 0) : len - (open ? 2 : 1)]) {
+ p = pointsx[i];
+ arc = arcs[i];
+ translate([p.x, p.y]) rotate([0, 0, arc[1]]) {
+ mirrored = xor(twisted[i], p[2] < 0) ? 0 : 1;
+ color(tooth_colour) rotate_extrude(angle = arc[0]) translate([abs(p[2]), 0, 0]) mirror([mirrored, 0, 0]) beltp();
+ color(belt_colour) rotate_extrude(angle = arc[0]) translate([abs(p[2]), 0, 0]) mirror([mirrored, 0, 0]) beltb();
+ }
+ }
}
-function belt_length(points, gap = 0) = rounded_polygon_length(points, rounded_polygon_tangents(points)) - gap; //! Compute belt length given path and optional gap
+function _belt_points_info(type, points, open, twist, auto_twist, start_twist) = //! Helper function that calculates [twist, istwisted, points, tangents, arcs]
+let(
+ len = len(points),
+ isleft = function(i) let(
+ p = vec2(points[i]),
+ p0 = vec2(points[(i - 1 + len) % len]),
+ p1 = vec2(points[(i + 1) % len])
+ ) cross(p-p0,p1-p) > 0,
+ dotwist = function(i, istwisted) let( in = (i + 1) % len )
+ is_list(twist) ? twist[i] :
+ !is_undef(twist) ? i == twist :
+ open && is_list(points[in][2]) && auto_twist ? !pulley_teeth(points[in][2]) && !xor(isleft(in), istwisted) :
+ false,
+ twisted = [
+ for (
+ i = 0,
+ istwisted = start_twist,
+ twist = dotwist(i, istwisted),
+ nexttwisted = xor(twist, istwisted);
+ i < len;
+ i = i + 1,
+ istwisted = nexttwisted,
+ twist = dotwist(i, istwisted),
+ nexttwisted = xor(twist, istwisted)
+ ) [twist, istwisted] ],
+ pointsx = mapi(points, function(i, p) !is_list(p[2]) ? p : [p.x, p.y, let( // if p[2] is not a list it is just r, otherwise it is taken to be a pulley and we calculate r
+ isleft = isleft(i),
+ r = belt_pulley_pr(type, p[2], twisted = !xor(pulley_teeth(p[2]), xor(isleft, twisted[i][1])))
+ ) isleft ? -r : r ] ),
+ tangents = rounded_polygon_tangents(pointsx),
+ arcs = rounded_polygon_arcs(pointsx, tangents)
+) [ [ for (t = twisted) t[0] ], [ for (t = twisted) t[1] ], pointsx, tangents, arcs];
+
+function belt_pulley_pr(type, pulley, twisted=false) = //! Pitch radius. Default it expects the belt tooth to be against a toothed pulley an the backside to be against a smooth pulley (an idler). If `twisted` is true, the the belt is the other way around.
+ let(
+ thickness = belt_thickness(type),
+ ph = belt_pitch_height(type)
+ ) pulley_teeth(pulley)
+ ? pulley_pr(pulley) + (twisted ? thickness - ph : 0 )
+ : pulley_ir(pulley) + (twisted ? ph : thickness - ph );
+
+
+function belt_length(type, points, open = false) = _belt_length(_belt_points_info(type, points, open), open); //! Compute belt length given path
+
+function _belt_length(info, open) = let(
+ len = len(info[0]),
+ d = open ? 1 : 0,
+ tangents = slice(info[3], 0, len - d) ,
+ arcs = slice(info[4], d, len - d)
+) sumv( map( concat(tangents, arcs), function(e) e[2] ));