1
0
mirror of https://github.com/nophead/NopSCADlib.git synced 2025-08-20 06:11:41 +02:00

Added screw threads to most things that are threaded.

Added a mechanism for tests.py and views.py to have command line options.
This commit is contained in:
Chris Palmer
2020-02-22 19:44:01 +00:00
parent 1614f50b73
commit e068918e21
76 changed files with 650 additions and 242 deletions

View File

@@ -39,9 +39,9 @@ function rotate(a, v) = //! Generate a 4x4 rotation matrix, ```a``` can be a vec
sy = sin(av[1]),
sz = sin(av[2]))
[
[ cy * cz, cz * sx * sy - cx * sz, cx * cz * sy + sx * sz, 0],
[ cy * sz, cx * cz + sx * sy * sz,-cz * sx + cx * sy * sz, 0],
[-sy, cy * sx, cx * cy, 0],
[ cy * cz, sx * sy * cz - cx * sz, cx * sy * cz + sx * sz, 0],
[ cy * sz, sx * sy * sz + cx * cz, cx * sy * sz - sx * cz, 0],
[-sy, sx * cy, cx * cy, 0],
[ 0, 0, 0, 1]
]
: let(s = sin(a),
@@ -65,6 +65,7 @@ function scale(v) = let(s = is_list(v) ? v : [v, v, v]) //! Generate a 4x4 matr
];
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
function transform_points(path, m) = [for(p = path) transform(p, m)]; //! Apply transform to a path
function unit(v) = let(n = norm(v)) n ? v / n : v; //! Convert ```v``` to a unit vector
@@ -74,3 +75,11 @@ function transpose(m) = [ for(j = [0 : len(m[0]) - 1]) [ for(i = [0 : len(m) - 1
function identity(n, x = 1) = [for(i = [0 : n - 1]) [for(j = [0 : n - 1]) i == j ? x : 0] ]; //! Construct an arbitrary size identity matrix
function reverse(v) = let(n = len(v) - 1) n < 0 ? [] : [for(i = [0 : n]) v[n - i]]; //! Reverse a vector
function angle_between(v1, v2) = acos(v1 * v2 / (norm(v1) * norm(v2))); //! Return the angle between two vectors
// https://www.gregslabaugh.net/publications/euler.pdf
function euler(R) = let(ay = asin(-R[2][0]), cy = cos(ay)) //! Convert a rotation matrix to a Euler rotation vector.
cy ? [ atan2(R[2][1] / cy, R[2][2] / cy), ay, atan2(R[1][0] / cy, R[0][0] / cy) ]
: R[2][0] < 0 ? [atan2( R[0][1], R[0][2]), 180, 0]
: [atan2(-R[0][1], -R[0][2]), -180, 0];

View File

@@ -62,6 +62,7 @@ function rotate_from_to(a, b) =
function calculate_twist(A, B) = let(D = transpose3(B) * A) atan2(D[1][0], D[0][0]);
//
// Compute a 4x3 matrix to orientate a frame of the sweep given the position and a 3x3 rotation matrix.
// Note that the rotation matrix is transposed to allow post multiplication.
//
function orientate(p, r) =
let(x = r[0], y = r[1], z = r[2])
@@ -79,12 +80,21 @@ function rot3_z(a) =
[ [ c, -s, 0],
[ s, c, 0],
[ 0, 0, 1] ];
//
// Calculate the unit tangent at a vertex given the indices before and after. One of these can be the same as i in the case
// of the start and end of a non closed path.
// of the start and end of a non closed path. Note that the edges are converted to unit vectors so that their relative lengths
// don't affect the direction of the tangent.
//
function tangent(path, before, i, after) = unit(unit(path[after] - path[i]) - unit(path[before] - path[i]));
function tangent(path, before, i, after) = unit(unit(path[i] - path[before]) + unit(path[after] - path[i]));
//
// Calculate the twist per segment caused by rotate_from_to() instead of a simple Euler rotation around Z.
//
function helical_twist_per_segment(r, pitch, sides) = //! Calculate the twist around Z that rotate_from_to() introduces
let(step_angle = 360 / sides,
lt = 2 * r * sin(step_angle), // length of tangent between two facets
slope = atan(2 * pitch / sides / lt) // slope of tangents
) step_angle * sin(slope); // angle tangent should rotate around z projected onto axis rotate_from_to() uses
//
// Generate all the surface points of the swept volume.
//
@@ -111,24 +121,28 @@ function skin_points(profile, path, loop, twist = 0) =
each profile4 * orientate(path[i], rotations[i] * rot3_z(za))
];
function cap(facets, segment = 0) = [for(i = [0 : facets - 1]) segment ? facets * segment + i : facets - 1 - i];
function cap(facets, segment = 0, end) = //! Create the mesh for an end cap
let(reverse = is_undef(end) ? segment : end)
[for(i = [0 : facets - 1]) facets * segment + (reverse ? i : facets - 1 - i)];
function quad(p, a, b, c, d) = norm(p[a] - p[c]) > norm(p[b] - p[d]) ? [[b, c, d], [b, d, a]] : [[a, b, c], [a, c, d]];
function skin_faces(points, segs, facets, loop) = [for(i = [0 : facets - 1], s = [0 : segs - (loop ? 1 : 2)])
each quad(points,
s * facets + i,
s * facets + (i + 1) % facets,
((s + 1) % segs) * facets + (i + 1) % facets,
((s + 1) % segs) * facets + i)];
function skin_faces(points, npoints, facets, loop, offset = 0) = //! Create the mesh for the swept volume without end caps
[for(i = [0 : facets - 1], s = [0 : npoints - (loop ? 1 : 2)])
let(j = s + offset, k = loop ? (j + 1) % npoints : j + 1)
each quad(points,
j * facets + i,
j * facets + (i + 1) % facets,
k * facets + (i + 1) % facets,
k * facets + i)];
function sweep(path, profile, loop = false, twist = 0) = //! Generate the point list and face list of the swept volume
let(
segments = len(path),
npoints = len(path),
facets = len(profile),
points = skin_points(profile, path, loop, twist),
skin_faces = skin_faces(points, segments, facets, loop),
faces = loop ? skin_faces : concat([cap(facets)], skin_faces, [cap(facets, segments - 1)])
skin_faces = skin_faces(points, npoints, facets, loop),
faces = loop ? skin_faces : concat([cap(facets)], skin_faces, [cap(facets, npoints - 1)])
) [points, faces];
module sweep(path, profile, loop = false, twist = 0) { //! Draw a polyhedron that is the swept volume
@@ -141,9 +155,9 @@ function path_length(path, i = 0, length = 0) = //! Calculated the length along
i >= len(path) - 1 ? length
: path_length(path, i + 1, length + norm(path[i + 1] - path[i]));
function circle_points(r = 1, z = 0) = //! Generate the points of a circle, setting z makes a single turn spiral
function circle_points(r = 1, z = 0, dir = -1) = //! Generate the points of a circle, setting z makes a single turn spiral
let(sides = r2sides(r))
[for(i = [0 : sides - 1]) let(a = i * 360 / sides) [r * sin(a), r * cos(a), z * a / 360]];
[for(i = [0 : sides - 1]) let(a = dir * i * 360 / sides) [r * cos(a), r * sin(a), z * i / sides]];
function rectangle_points(w, h) = [[-w/2, -h/2, 0], [-w/2, h/2, 0], [w/2, h/2, 0], [w/2, -h/2, 0]]; //! Generate the points of a rectangle

View File

@@ -18,69 +18,169 @@
//
//
//! A utilities for making threads with sweep.
//! Utilities for making threads with sweep. They can be used to model screws, nuts, studding, leadscrews, etc, and also to make printed threads.
//!
//! The ends can be tapered, flat or chamfered by setting the ```top``` and ```bot``` parameters to -1 for tapered, 0 for a flat cut and positive to
//! specify a chamfer angle.
//!
//! Threads are by default solid, so the male version is wrapped around a cylinder and the female inside a tube. This can be suppressed to just get the helix, for
//! example to make a printed pot with a screw top lid.
//!
//! Threads with a typical 60 degree angle appear too bright with OpenSCAD's primitive lighting model as they face towards the lights more than the top and sides of
//! a cylinder. To get around this a colour can be passed to thread that is used to colour the cylinder and then toned down to colour the helix.
//!
//! Making the ends requires a CGAL intersection, which make threads relatively slow. For this reason they are generally disabled when using the GUI but can
//! be enabled by setting ```$show_threads``` to ```true```. When the tests are run, by default, threads are enabled only for things that feature them like screws.
//! This behaviour can be changed by setting a ```SHOW_THREADS``` environment variable to ```false``` to disable all threads and ```true``` to enable all threads.
//! The same variable also affects the generation of assembly diagrams.
//!
//! Threads obey the $fn, $fa, $fs variables.
//
include <../core.scad>
use <sweep.scad>
use <maths.scad>
use <tube.scad>
function thread_profile(h, crest, angle) = //! Create thread profile path
let(base = crest + 2 * h * tan(angle / 2))
[[-base / 2, 0, 0], [-crest / 2, h, 0], [crest / 2, h, 0], [base / 2, 0, 0]];
thread_colour_factor = 0.8; // 60 degree threads appear too bright due to the angle facing the light sources
function thread_profile(h, crest, angle, overlap = 0.1) = //! Create thread profile path
let(base = crest + 2 * (h + overlap) * tan(angle / 2))
[[-base / 2, -overlap, 0], [-crest / 2, h, 0], [crest / 2, h, 0], [base / 2, -overlap, 0]];
module thread(dia, pitch, length, profile, center = true, top = -1, bot = -1, starts = 1, solid = true, female = false, colour = undef) { //! Create male or femail thread, ends can be tapered, chamfered or square
//
// Apply colour if defined
//
module colour(factor) if(is_undef(colour)) children(); else color(colour * factor) children();
//
// Compress the profile to compensate for it being tilted by the helix angle
//
scale = cos(atan(pitch / (PI * dia)));
sprofile = [for(p = profile) [p.x * scale, p.y, p.z]];
//
// Extract some properties from the profile, perhaps they should be stored in it.
//
h = max([for(p = sprofile) p.y]);
maxx = max([for(p = sprofile) p.x]);
minx = min([for(p = sprofile) p.x]);
crest_xmax = max([for(p = sprofile) if(p.x != maxx) p.x]);
crest_xmin = min([for(p = sprofile) if(p.x != minx) p.x]);
//
// If the ends don't taper we need an extra half turn past the ends to be cropped horizontally.
//
extra_top = top < 0 ? 0 : -minx / pitch;
extra_bot = bot < 0 ? 0 : maxx / pitch;
turns = length / pitch + extra_top + extra_bot;
//
// Generate the helix path, possibly with tapered ends
//
dir = female ? 1 : -1;
r = dia / 2;
sides = r2sides4n(r);
step_angle = 360 / sides;
segs = ceil(turns * sides);
leadin = ceil(sides / starts);
final = floor(turns * sides) - leadin;
path = [for(i = [0 : segs],
R = i < leadin && bot < 0 ? r + dir * (h - h * i / leadin)
: i > final && top < 0 ? r + dir * h * (i - final) / leadin : r,
a = i * step_angle - 360 * extra_bot)
[R * cos(a), R * sin(a), a * pitch / 360]];
//
// Generate the skin vertices
//
facets = len(profile);
twist = helical_twist_per_segment(r, pitch, sides);
//
// For female threads we need to invert the profile
//
iprofile = female ? reverse([for(p = sprofile) [p.x, -p.y, 0]]) : sprofile;
//
// If the bottom is tapered then the twist will be greater, so pre-twist the profile to get the straight bit at the correct angle
//
rprofile = bot < 0 ? transform_points(iprofile, rotate(-dir * (helical_twist_per_segment(r - h, pitch, sides) - twist) * sides / PI))
: iprofile;
points = skin_points(rprofile, path, false, twist * segs);
//
// To form the ends correctly we need to use intersection but it is very slow with the full thread so we just
// intersect the start and the end and sweep the rest outside of the intersection.
//
top_chamfer_h = (top > 0 ? h * tan(top) : 0);
bot_chamfer_h = (bot > 0 ? h * tan(bot) : 0);
top_overlap = max( maxx, top_chamfer_h - crest_xmin) / pitch;
bot_overlap = max(-minx, bot_chamfer_h + crest_xmax) / pitch;
start = ceil(sides * (bot_overlap + extra_bot));
end = segs - ceil(sides * (top_overlap + extra_top));
start_skin_faces = skin_faces(points, start + 1, facets, false);
middle_skin_faces = skin_faces(points, end - start + 1, facets, false, start);
end_skin_faces = skin_faces(points, segs - end + 1, facets, false, end);
start_faces = concat([cap(facets) ], start_skin_faces, [cap(facets, start)]);
middle_faces = concat([cap(facets, start, false)], middle_skin_faces, [cap(facets, end)]);
end_faces = concat([cap(facets, end, false)], end_skin_faces, [cap(facets, segs)]);
overlap = - profile[0].y;
translate_z((center ? -length / 2 : 0)) {
ends_faces = concat(start_faces, end_faces);
for(i = [0 : starts - 1])
colour(thread_colour_factor)
rotate(360 * i / starts + (female ? 180 / starts : 0)) {
render() intersection() {
polyhedron(points, ends_faces);
len = length - 2 * eps;
rotate_extrude()
if(female) {
difference() {
translate([0, eps])
square([r + h + overlap, len]);
if(top_chamfer_h)
polygon([[0, length], [r, length], [r - h, length - top_chamfer_h], [0, length - top_chamfer_h]]);
if(bot_chamfer_h)
polygon([[0, 0], [r, 0], [r - h, bot_chamfer_h], [0, bot_chamfer_h]]);
}
}
else
difference() {
hull() {
translate([0, eps])
square([r, len]);
translate([0, bot_chamfer_h])
square([r + h + overlap, len - top_chamfer_h - bot_chamfer_h]);
}
if(!solid)
square([r - overlap, length]);
}
}
polyhedron(points, middle_faces);
}
module male_thread(pitch, minor_d, length, profile, taper_top = true, center = true, solid = true) { //! Create male thread
turns = length / pitch + (taper_top ? 0 : 1);
r = minor_d / 2;
sides = r2sides(r);
h = max([for(p = profile) p.y]);
final = (turns - 1) * sides;
path = [for(i = [0 : sides * turns],
R = i < sides ? r - h + h * i / sides
: i > final && taper_top ? r - h * (i - final) / sides : r,
a = i * 360 / sides)
[R * sin(-a), R * cos(-a), pitch * a / 360]];
t = atan(pitch / sides / (r * cos(225 / sides)));
translate_z(center ? -length / 2 : 0) {
render() intersection() {
sweep(path, profile, twist = t * sides * turns);
cylinder(d = minor_d + 5, h = length);
}
if(solid)
rotate(90)
cylinder(d = minor_d + eps, h = length);
colour(1)
rotate(90)
if(female)
tube(or = r + (top < 0 || bot < 0 ? h : 0) + 2 * overlap, ir = r, h = length, center = false);
else
cylinder(d = dia, h = length);
}
}
module female_thread(pitch, outer_d, length, profile, taper_top = true, center = true) { //! Create female thread
turns = length / pitch + (taper_top ? 0 : 1);
r = outer_d / 2;
sides = r2sides(r);
h = max([for(p = profile) p.y]);
final = (turns - 1) * sides;
path = [for(i = [0 : sides * turns],
R = i < sides ? r + h - h * i / sides
: i > final && taper_top ? r + h * (i - final) / sides : r,
a = i * 360 / sides)
[R * sin(-a), R * cos(-a), pitch * a / 360]];
t = atan(pitch / sides / (r * cos(225 / sides)));
translate_z(center ? -length / 2 : 0) {
render() intersection() {
sweep(path, reverse([for(p = profile) [p.x, -p.y, 0]]), twist = t * sides * turns);
cylinder(d = outer_d + 5, h = length);
}
}
module male_metric_thread(d, pitch, length, center = true, top = -1, bot = -1, solid = true, colour = undef) { //! Create male thread with metric profile
H = pitch * sqrt(3) / 2;
h = 5 * H / 8;
minor_d = d - 2 * h;
thread(minor_d, pitch, length, thread_profile(h, pitch / 8, 60), center, top, bot, solid = solid, colour = colour);
}
module male_metric_thread(d, pitch, length, taper_top = true, center = true) { //! Create male thread with metric profile
h = sqrt(3) / 2 * pitch;
minor_d = d - 5 * h / 4;
male_thread(pitch, minor_d, length, thread_profile((d - minor_d) / 2, pitch / 8, 60), taper_top, center);
}
module female_metric_thread(d, pitch, length, taper_top = true, center = true) { //! Create male thread with metric profile
h = sqrt(3) / 2 * pitch;
outer_d = d + 5 * h / 4;
male_thread(pitch, outer_d, length, thread_profile((outer_d - d) / 2, pitch / 8, 60), taper_top, center);
module female_metric_thread(d, pitch, length, center = true, top = -1, bot = -1, colour = undef) { //! Create female thread with metric profile
H = pitch * sqrt(3) / 2;
h = 5 * H / 8;
thread(d, pitch, length, thread_profile(h, pitch / 4, 60), center, top, bot, solid = false, female = true, colour = colour);
}
function metric_coarse_pitch(d) //! Convert metric diameter to pitch
@@ -106,9 +206,12 @@ function metric_coarse_pitch(d) //! Convert metric diameter to pitch
0,
0,
1.75, // M12
0,
0,
0,
0, // M14
0,
0,
0,
2.0, // M16
][d * 2 - 4];
male_metric_thread(3, 0.5, 25);
translate([10, 0])
male_metric_thread(8, 1.25, 30);