From 6e3efd68a41bdc000737959bf4e94441ea658336 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 1 Oct 2021 00:30:28 -0400 Subject: [PATCH] big function re-org to eliminate debug.scad and hide affine.scad from docs --- .openscad_gendocs_rc | 1 + affine.scad | 468 ----------------------------------------- arrays.scad | 25 +++ attachments.scad | 176 ++++++++++++++++ debug.scad | 350 ------------------------------ geometry.scad | 61 ++++++ math.scad | 74 +++++-- shapes3d.scad | 96 +++++++++ skin.scad | 214 +++++++++++++++++++ std.scad | 1 - tests/test_affine.scad | 19 -- tests/test_debug.scad | 11 - tests/test_math.scad | 12 -- transforms.scad | 80 +++++++ 14 files changed, 707 insertions(+), 881 deletions(-) delete mode 100644 debug.scad delete mode 100644 tests/test_debug.scad diff --git a/.openscad_gendocs_rc b/.openscad_gendocs_rc index 17e0fdf..482b175 100644 --- a/.openscad_gendocs_rc +++ b/.openscad_gendocs_rc @@ -1,5 +1,6 @@ DocsDirectory: BOSL2.wiki/ IgnoreFiles: + affine.scad foo.scad std.scad bosl1compat.scad diff --git a/affine.scad b/affine.scad index c222ffe..7867628 100644 --- a/affine.scad +++ b/affine.scad @@ -6,474 +6,6 @@ ////////////////////////////////////////////////////////////////////// -// Section: Matrix Manipulation - -// Function: ident() -// Usage: -// mat = ident(n); -// Topics: Affine, Matrices -// Description: -// Create an `n` by `n` square identity matrix. -// Arguments: -// n = The size of the identity matrix square, `n` by `n`. -// Example: -// mat = ident(3); -// // Returns: -// // [ -// // [1, 0, 0], -// // [0, 1, 0], -// // [0, 0, 1] -// // ] -// Example: -// mat = ident(4); -// // Returns: -// // [ -// // [1, 0, 0, 0], -// // [0, 1, 0, 0], -// // [0, 0, 1, 0], -// // [0, 0, 0, 1] -// // ] -function ident(n) = [ - for (i = [0:1:n-1]) [ - for (j = [0:1:n-1]) (i==j)? 1 : 0 - ] -]; - - -// Function: is_affine() -// Usage: -// bool = is_affine(x, [dim]); -// Topics: Affine, Matrices, Transforms, Type Checking -// See Also: is_matrix() -// Description: -// Tests if the given value is an affine matrix, possibly also checking it's dimenstion. -// Arguments: -// x = The value to test for being an affine matrix. -// dim = The number of dimensions the given affine is required to be for. Generally 2 for 2D or 3 for 3D. If given as a list of integers, allows any of the given dimensions. Default: `[2,3]` -// Example: -// bool = is_affine(affine2d_scale([2,3])); // Returns true -// bool = is_affine(affine3d_scale([2,3,4])); // Returns true -// bool = is_affine(affine3d_scale([2,3,4]),2); // Returns false -// bool = is_affine(affine3d_scale([2,3]),2); // Returns true -// bool = is_affine(affine3d_scale([2,3,4]),3); // Returns true -// bool = is_affine(affine3d_scale([2,3]),3); // Returns false -function is_affine(x,dim=[2,3]) = - is_finite(dim)? is_affine(x,[dim]) : - let( ll = len(x) ) - is_list(x) && in_list(ll-1,dim) && - [for (r=x) if(!is_list(r) || len(r)!=ll) 1] == []; - - -// Function: is_2d_transform() -// Usage: -// x = is_2d_transform(t); -// Topics: Affine, Matrices, Transforms, Type Checking -// See Also: is_affine(), is_matrix() -// Description: -// Checks if the input is a 3D transform that does not act on the z coordinate, except possibly -// for a simple scaling of z. Note that an input which is only a zscale returns false. -// Arguments: -// t = The transformation matrix to check. -// Example: -// b = is_2d_transform(zrot(45)); // Returns: true -// b = is_2d_transform(yrot(45)); // Returns: false -// b = is_2d_transform(xrot(45)); // Returns: false -// b = is_2d_transform(move([10,20,0])); // Returns: true -// b = is_2d_transform(move([10,20,30])); // Returns: false -// b = is_2d_transform(scale([2,3,4])); // Returns: true -function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][2]!=1 so scale() works - t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && - (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() - - -// Function: affine2d_to_3d() -// Usage: -// mat = affine2d_to_3d(m); -// Topics: Affine, Matrices, Transforms -// See Also: affine3d_to_2d() -// Description: -// Takes a 3x3 affine2d matrix and returns its 4x4 affine3d equivalent. -// Example: -// mat = affine2d_to_3d(affine2d_translate([10,20])); -// // Returns: -// // [ -// // [1, 0, 0, 10], -// // [0, 1, 0, 20], -// // [0, 0, 1, 0], -// // [0, 0, 0, 1], -// // ] -function affine2d_to_3d(m) = [ - [ m[0][0], m[0][1], 0, m[0][2] ], - [ m[1][0], m[1][1], 0, m[1][2] ], - [ 0, 0, 1, 0 ], - [ m[2][0], m[2][1], 0, m[2][2] ] -]; - - -// Function: affine3d_to_2d() -// Usage: -// mat = affine3d_to_2d(m); -// Topics: Affine, Matrices -// See Also: affine2d_to_3d() -// Description: -// Takes a 4x4 affine3d matrix and returns its 3x3 affine2d equivalent. 3D transforms that would alter the Z coordinate are disallowed. -// Example: -// mat = affine2d_to_3d(affine3d_translate([10,20,0])); -// // Returns: -// // [ -// // [1, 0, 10], -// // [0, 1, 20], -// // [0, 0, 1], -// // ] -function affine3d_to_2d(m) = - assert(is_2d_transform(m)) - [ - for (r=[0:3]) if (r!=2) [ - for (c=[0:3]) if (c!=2) m[r][c] - ] - ]; - - -// Function: apply() -// Usage: -// pts = apply(transform, points); -// Topics: Affine, Matrices, Transforms -// Description: -// Applies the specified transformation matrix to a point, pointlist, bezier patch or VNF. -// Both inputs can be 2D or 3D, and it is also allowed to supply 3D transformations with 2D -// data as long as the the only action on the z coordinate is a simple scaling. -// . -// If you construct your own matrices you can also use a transform that acts like a projection -// with fewer rows to produce lower dimensional output. -// Arguments: -// transform = The 2D or 3D transformation matrix to apply to the point/points. -// points = The point, pointlist, bezier patch, or VNF to apply the transformation to. -// Example(3D): -// path1 = path3d(circle(r=40)); -// tmat = xrot(45); -// path2 = apply(tmat, path1); -// #stroke(path1,closed=true); -// stroke(path2,closed=true); -// Example(2D): -// path1 = circle(r=40); -// tmat = translate([10,5]); -// path2 = apply(tmat, path1); -// #stroke(path1,closed=true); -// stroke(path2,closed=true); -// Example(2D): -// path1 = circle(r=40); -// tmat = rot(30) * back(15) * scale([1.5,0.5,1]); -// path2 = apply(tmat, path1); -// #stroke(path1,closed=true); -// stroke(path2,closed=true); -function apply(transform,points) = - points==[] ? [] : - is_vector(points) - ? /* Point */ apply(transform, [points])[0] : - is_list(points) && len(points)==2 && is_path(points[0],3) && is_list(points[1]) && is_vector(points[1][0]) - ? /* VNF */ [apply(transform, points[0]), points[1]] : - is_list(points) && is_list(points[0]) && is_vector(points[0][0]) - ? /* BezPatch */ [for (x=points) apply(transform,x)] : - let( - tdim = len(transform[0])-1, - datadim = len(points[0]), - outdim = min(datadim,len(transform)), - matrix = [for(i=[0:1:tdim]) [for(j=[0:1:outdim-1]) transform[j][i]]] - ) - tdim==datadim && (datadim==3 || datadim==2) ? [for(p=points) concat(p,1)] * matrix - : tdim == 3 && datadim == 2 ? - assert(is_2d_transform(transform), str("Transforms is 3d but points are 2d")) - [for(p=points) concat(p,[0,1])]*matrix - : assert(false, str("Unsupported combination: transform with dimension ",tdim,", data of dimension ",datadim)); - - -// Function: rot_decode() -// Usage: -// info = rot_decode(rotation,[long]); // Returns: [angle,axis,cp,translation] -// Topics: Affine, Matrices, Transforms -// Description: -// Given an input 3D rigid transformation operator (one composed of just rotations and translations) represented -// as a 4x4 matrix, compute the rotation and translation parameters of the operator. Returns a list of the -// four parameters, the angle, in the interval [0,180], the rotation axis as a unit vector, a centerpoint for -// the rotation, and a translation. If you set `parms = rot_decode(rotation)` then the transformation can be -// reconstructed from parms as `move(parms[3]) * rot(a=parms[0],v=parms[1],cp=parms[2])`. This decomposition -// makes it possible to perform interpolation. If you construct a transformation using `rot` the decoding -// may flip the axis (if you gave an angle outside of [0,180]). The returned axis will be a unit vector, and -// the centerpoint lies on the plane through the origin that is perpendicular to the axis. It may be different -// than the centerpoint you used to construct the transformation. -// . -// If you set `long` to true then return the reversed rotation, with the angle in [180,360]. -// Arguments: -// rotation = rigid transformation to decode -// long = if true return the "long way" around, with the angle in [180,360]. Default: false -// Example: -// info = rot_decode(rot(45)); -// // Returns: [45, [0,0,1], [0,0,0], [0,0,0]] -// Example: -// info = rot_decode(rot(a=37, v=[1,2,3], cp=[4,3,-7]))); -// // Returns: [37, [0.26, 0.53, 0.80], [4.8, 4.6, -4.6], [0,0,0]] -// Example: -// info = rot_decode(left(12)*xrot(-33)); -// // Returns: [33, [-1,0,0], [0,0,0], [-12,0,0]] -// Example: -// info = rot_decode(translate([3,4,5])); -// // Returns: [0, [0,0,1], [0,0,0], [3,4,5]] -function rot_decode(M,long=false) = - assert(is_matrix(M,4,4) && approx(M[3],[0,0,0,1]), "Input matrix must be a 4x4 matrix representing a 3d transformation") - let(R = submatrix(M,[0:2],[0:2])) - assert(approx(det3(R),1) && approx(norm_fro(R * transpose(R)-ident(3)),0),"Input matrix is not a rotation") - let( - translation = [for(row=[0:2]) M[row][3]], // translation vector - largest = max_index([R[0][0], R[1][1], R[2][2]]), - axis_matrix = R + transpose(R) - (matrix_trace(R)-1)*ident(3), // Each row is on the rotational axis - // Construct quaternion q = c * [x sin(theta/2), y sin(theta/2), z sin(theta/2), cos(theta/2)] - q_im = axis_matrix[largest], - q_re = R[(largest+2)%3][(largest+1)%3] - R[(largest+1)%3][(largest+2)%3], - c_sin = norm(q_im), // c * sin(theta/2) for some c - c_cos = abs(q_re) // c * cos(theta/2) - ) - approx(c_sin,0) ? [0,[0,0,1],[0,0,0],translation] : - let( - angle = 2*atan2(c_sin, c_cos), // This is supposed to be more accurate than acos or asin - axis = (q_re>=0 ? 1:-1)*q_im/c_sin, - tproj = translation - (translation*axis)*axis, // Translation perpendicular to axis determines centerpoint - cp = (tproj + cross(axis,tproj)*c_cos/c_sin)/2 - ) - [long ? 360-angle:angle, - long? -axis : axis, - cp, - (translation*axis)*axis]; - - -// Function: rot_inverse() -// Usage: -// B = rot_inverse(A) -// Description: -// Inverts a 2d (3x3) or 3d (4x4) rotation matrix. The matrix can be a rotation around any center, -// so it may include a translation. -function rot_inverse(T) = - assert(is_matrix(T,square=true),"Matrix must be square") - let( n = len(T)) - assert(n==3 || n==4, "Matrix must be 3x3 or 4x4") - let( - rotpart = [for(i=[0:n-2]) [for(j=[0:n-2]) T[j][i]]], - transpart = [for(row=[0:n-2]) T[row][n-1]] - ) - assert(approx(determinant(T),1),"Matrix is not a rotation") - concat(hstack(rotpart, -rotpart*transpart),[[for(i=[2:n]) 0, 1]]); - - -function _closest_angle(alpha,beta) = - is_vector(beta) ? [for(entry=beta) _closest_angle(alpha,entry)] - : beta-alpha > 180 ? beta - ceil((beta-alpha-180)/360) * 360 - : beta-alpha < -180 ? beta + ceil((alpha-beta-180)/360) * 360 - : beta; - - -// Smooth data with N point moving average. If angle=true handles data as angles. -// If closed=true assumes last point is adjacent to the first one. -// If closed=false pads data with left/right value (probably wrong behavior...should do linear interp) -function _smooth(data,len,closed=false,angle=false) = - let( halfwidth = floor(len/2), - result = closed ? [for(i=idx(data)) - let( - window = angle ? _closest_angle(data[i],select(data,i-halfwidth,i+halfwidth)) - : select(data,i-halfwidth,i+halfwidth) - ) - mean(window)] - : [for(i=idx(data)) - let( - window = select(data,max(i-halfwidth,0),min(i+halfwidth,len(data)-1)), - left = i-halfwidth<0, - pad = left ? data[0] : last(data) - ) - sum(window)+pad*(len-len(window))] / len - ) - result; - -// Function: rot_resample() -// Usage: -// rlist = rot_resample(rotlist, N, [method], [twist], [scale], [smoothlen], [long], [turns], [closed]) -// Description: -// Takes as input a list of rotation matrices in 3d. Produces as output a resampled -// list of rotation operators (4x4 matrixes) suitable for use with sweep(). You can optionally apply twist to -// the output with the twist parameter, which is either a scalar to apply a uniform -// overall twist, or a vector to apply twist non-uniformly. Similarly you can apply -// scaling either overall or with a vector. The smoothlen parameter applies smoothing -// to the twist and scaling to prevent abrupt changes. This is done by a moving average -// of the smoothing or scaling values. The default of 1 means no smoothing. The long parameter causes -// the interpolation to be done the "long" way around the rotation instead of the short way. -// Note that the rotation matrix cannot distinguish which way you rotate, only the place you -// end after rotation. Another ambiguity arises if your rotation is more than 360 degrees. -// You can add turns with the turns parameter, so giving turns=1 will add 360 degrees to the -// rotation so it completes one full turn plus the additional rotation given my the transform. -// You can give long as a scalar or as a vector. Finally if closed is true then the -// resampling will connect back to the beginning. -// . -// The default is to resample based on the length of the arc defined by each rotation operator. This produces -// uniform sampling over all of the transformations. It requires that each rotation has nonzero length. -// In this case N specifies the total number of samples. If you set method to "count" then N you get -// N samples for each transform. You can set N to a vector to vary the samples at each step. -// Arguments: -// rotlist = list of rotation operators in 3d to resample -// N = Number of rotations to produce as output when method is "length" or number for each transformation if method is "count". Can be a vector when method is "count" -// -- -// method = sampling method, either "length" or "count" -// twist = scalar or vector giving twist to add overall or at each rotation. Default: none -// scale = scalar or vector giving scale factor to add overall or at each rotation. Default: none -// smoothlen = amount of smoothing to apply to scaling and twist. Should be an odd integer. Default: 1 -// long = resample the "long way" around the rotation, a boolean or list of booleans. Default: false -// turns = add extra turns. If a scalar adds the turns to every rotation, or give a vector. Default: 0 -// closed = if true then the rotation list is treated as closed. Default: false -// Example: Resampling the arc from a compound rotation with translations thrown in. -// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25); -// sweep(circle(r=1,$fn=3), tran); -// Example: Applying a scale factor -// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25, scale=2); -// sweep(circle(r=1,$fn=3), tran); -// Example: Applying twist -// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25, twist=60); -// sweep(circle(r=1,$fn=3), tran); -// Example: Going the long way -// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25, long=true); -// sweep(circle(r=1,$fn=3), tran); -// Example: Getting transformations from turtle3d -// include -// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,170],transforms=true); -// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40)); -// Example: If you specify a larger angle in turtle you need to use the long argument -// include -// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270],transforms=true); -// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40,long=true)); -// Example: And if the angle is over 360 you need to add turns to get the right result. Note long is false when the remaining angle after subtracting full turns is below 180: -// include -// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,90+360],transforms=true); -// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40,long=false,turns=1)); -// Example: Here the remaining angle is 270, so long must be set to true -// include -// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270+360],transforms=true); -// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40,long=true,turns=1)); -// Example: Note the visible line at the scale transition -// include -// tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true); -// rtran = rot_resample(tran,200,scale=[1,6]); -// sweep(circle(1,$fn=32),rtran); -// Example: Observe how using a large smoothlen value eases that transition -// include -// tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true); -// rtran = rot_resample(tran,200,scale=[1,6],smoothlen=17); -// sweep(circle(1,$fn=32),rtran); -// Example: A similar issues can arise with twist, where a "line" is visible at the transition -// include -// tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]); -// rtran = rot_resample(tran,100,twist=[0,60],smoothlen=1); -// sweep(subdivide_path(rect([3,3]),40),rtran); -// Example: Here's the smoothed twist transition -// include -// tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]); -// rtran = rot_resample(tran,100,twist=[0,60],smoothlen=17); -// sweep(subdivide_path(rect([3,3]),40),rtran); -// Example: toothed belt based on list-comprehension-demos example. This version has a smoothed twist transition. Try changing smoothlen to 1 to see the more abrupt transition that occurs without smoothing. -// include -// r_small = 19; // radius of small curve -// r_large = 46; // radius of large curve -// flat_length = 100; // length of flat belt section -// teeth=42; // number of teeth -// belt_width = 12; -// tooth_height = 9; -// belt_thickness = 3; -// angle = 180 - 2*atan((r_large-r_small)/flat_length); -// beltprofile = path3d(subdivide_path( -// square([belt_width, belt_thickness],anchor=FWD), -// 20)); -// beltrots = -// turtle3d(["arcsteps",1, -// "move", flat_length, -// "arcleft", r_small, angle, -// "move", flat_length, -// // Closing path will be interpolated -// // "arcleft", r_large, 360-angle -// ],transforms=true); -// beltpath = rot_resample(beltrots,teeth*4, -// twist=[180,0,-180,0], -// long=[false,false,false,true], -// smoothlen=15,closed=true); -// belt = [for(i=idx(beltpath)) -// let(tooth = floor((i+$t*4)/2)%2) -// apply(beltpath[i]* -// yscale(tooth -// ? tooth_height/belt_thickness -// : 1), -// beltprofile) -// ]; -// skin(belt,slices=0,closed=true); -function rot_resample(rotlist,N,twist,scale,smoothlen=1,long=false,turns=0,closed=false,method="length") = - assert(is_int(smoothlen) && smoothlen>0 && smoothlen%2==1, "smoothlen must be a positive odd integer") - assert(method=="length" || method=="count") - let(tcount = len(rotlist) + (closed?0:-1)) - assert(method=="count" || is_int(N), "N must be an integer when method is \"length\"") - assert(is_int(N) || is_vector(N,tcount), str("N must be scalar or vector with length ",tcount)) - let( - count = method=="length" ? (closed ? N+1 : N) - : (is_vector(N) ? sum(N) : tcount*N)+1 //(closed?0:1) - ) - assert(is_bool(long) || len(long)==tcount,str("Input long must be a scalar or have length ",tcount)) - let( - long = force_list(long,tcount), - turns = force_list(turns,tcount), - T = [for(i=[0:1:tcount-1]) rot_inverse(rotlist[i])*select(rotlist,i+1)], - parms = [for(i=idx(T)) - let(tparm = rot_decode(T[i],long[i])) - [tparm[0]+turns[i]*360,tparm[1],tparm[2],tparm[3]] - ], - radius = [for(i=idx(parms)) norm(parms[i][2])], - length = [for(i=idx(parms)) norm([norm(parms[i][3]), parms[i][0]/360*2*PI*radius[i]])] - ) - assert(method=="count" || all_positive(length), - "Rotation list includes a repeated entry or a rotation around the origin, not allowed when method=\"length\"") - let( - cumlen = [0, each cumsum(length)], - totlen = last(cumlen), - stepsize = totlen/(count-1), - samples = method=="count" - ? let( N = force_list(N,tcount)) - [for(n=N) lerpn(0,1,n,endpoint=false)] - :[for(i=idx(parms)) - let( - remainder = cumlen[i] % stepsize, - offset = remainder==0 ? 0 - : stepsize-remainder, - num = ceil((length[i]-offset)/stepsize) - ) - count(num,offset,stepsize)/length[i]], - twist = first_defined([twist,0]), - scale = first_defined([scale,1]), - needlast = !approx(last(last(samples)),1), - sampletwist = is_num(twist) ? lerpn(0,twist,count) - : let( - cumtwist = [0,each cumsum(twist)] - ) - [for(i=idx(parms)) each lerp(cumtwist[i],cumtwist[i+1],samples[i]), - if (needlast) last(cumtwist) - ], - samplescale = is_num(scale) ? lerp(1,scale,lerpn(0,1,count)) - : let( - cumscale = [1,each cumprod(scale)] - ) - [for(i=idx(parms)) each lerp(cumscale[i],cumscale[i+1],samples[i]), - if (needlast) last(cumscale)], - smoothtwist = _smooth(closed?select(sampletwist,0,-2):sampletwist,smoothlen,closed=closed,angle=true), - smoothscale = _smooth(samplescale,smoothlen,closed=closed), - interpolated = [ - for(i=idx(parms)) - each [for(u=samples[i]) rotlist[i] * move(u*parms[i][3]) * rot(a=u*parms[i][0],v=parms[i][1],cp=parms[i][2])], - if (needlast) last(rotlist) - ] - ) - [for(i=idx(interpolated,e=closed?-2:-1)) interpolated[i]*zrot(smoothtwist[i])*scale(smoothscale[i])]; - - - // Section: Affine2d 3x3 Transformation Matrices diff --git a/arrays.scad b/arrays.scad index f6c9336..be3bd7c 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1985,4 +1985,29 @@ function is_matrix_symmetric(A,eps=1e-12) = approx(A,transpose(A), eps); + +// Function&Module: echo_matrix() +// Usage: +// echo_matrix(M, [description=], [sig=], [eps=]); +// dummy = echo_matrix(M, [description=], [sig=], [eps=]), +// Description: +// Display a numerical matrix in a readable columnar format with `sig` significant +// digits. Values smaller than eps display as zero. If you give a description +// it is displayed at the top. +function echo_matrix(M,description,sig=4,eps=1e-9) = + let( + horiz_line = chr(8213), + matstr = matrix_strings(M,sig=sig,eps=eps), + separator = str_join(repeat(horiz_line,10)), + dummy=echo(str(separator," ",is_def(description) ? description : "")) + [for(row=matstr) echo(row)] + ) + echo(separator); + +module echo_matrix(M,description,sig=4,eps=1e-9) +{ + dummy = echo_matrix(M,description,sig,eps); +} + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/attachments.scad b/attachments.scad index a16b40b..09ae640 100644 --- a/attachments.scad +++ b/attachments.scad @@ -1759,5 +1759,181 @@ function _attachment_is_shown(tags) = ) shown && !hidden; +// Section: Visualizing Anchors + +/// Internal Function: _standard_anchors() +/// Usage: +/// anchs = _standard_anchors([two_d]); +/// Description: +/// Return the vectors for all standard anchors. +/// Arguments: +/// two_d = If true, returns only the anchors where the Z component is 0. Default: false +function _standard_anchors(two_d=false) = [ + for ( + zv = [ + if (!two_d) TOP, + CENTER, + if (!two_d) BOTTOM + ], + yv = [FRONT, CENTER, BACK], + xv = [LEFT, CENTER, RIGHT] + ) xv+yv+zv +]; + + + +// Module: show_anchors() +// Usage: +// ... show_anchors([s], [std=], [custom=]); +// Description: +// Show all standard anchors for the parent object. +// Arguments: +// s = Length of anchor arrows. +// --- +// std = If true (default), show standard anchors. +// custom = If true (default), show custom anchors. +// Example(FlatSpin,VPD=333): +// cube(50, center=true) show_anchors(); +module show_anchors(s=10, std=true, custom=true) { + check = assert($parent_geom != undef) 1; + two_d = _attach_geom_2d($parent_geom); + if (std) { + for (anchor=_standard_anchors(two_d=two_d)) { + if(two_d) { + attach(anchor) anchor_arrow2d(s); + } else { + attach(anchor) anchor_arrow(s); + } + } + } + if (custom) { + for (anchor=last($parent_geom)) { + attach(anchor[0]) { + if(two_d) { + anchor_arrow2d(s, color="cyan"); + } else { + anchor_arrow(s, color="cyan"); + } + color("black") + noop($tags="anchor-arrow") { + xrot(two_d? 0 : 90) { + back(s/3) { + yrot_copies(n=2) + up(s/30) { + linear_extrude(height=0.01, convexity=12, center=true) { + text(text=anchor[0], size=s/4, halign="center", valign="center"); + } + } + } + } + } + color([1, 1, 1, 0.4]) + noop($tags="anchor-arrow") { + xrot(two_d? 0 : 90) { + back(s/3) { + zcopies(s/21) cube([s/4.5*len(anchor[0]), s/3, 0.01], center=true); + } + } + } + } + } + } + children(); +} + + + +// Module: anchor_arrow() +// Usage: +// anchor_arrow([s], [color], [flag]); +// Description: +// Show an anchor orientation arrow. By default, tagged with the name "anchor-arrow". +// Arguments: +// s = Length of the arrows. Default: `10` +// color = Color of the arrow. Default: `[0.333, 0.333, 1]` +// flag = If true, draw the orientation flag on the arrowhead. Default: true +// Example: +// anchor_arrow(s=20); +module anchor_arrow(s=10, color=[0.333,0.333,1], flag=true, $tags="anchor-arrow") { + $fn=12; + recolor("gray") spheroid(d=s/6) { + attach(CENTER,BOT) recolor(color) cyl(h=s*2/3, d=s/15) { + attach(TOP,BOT) cyl(h=s/3, d1=s/5, d2=0) { + if(flag) { + position(BOT) + recolor([1,0.5,0.5]) + cuboid([s/100, s/6, s/4], anchor=FRONT+BOT); + } + children(); + } + } + } +} + + + +// Module: anchor_arrow2d() +// Usage: +// anchor_arrow2d([s], [color], [flag]); +// Description: +// Show an anchor orientation arrow. +// Arguments: +// s = Length of the arrows. +// color = Color of the arrow. +// Example: +// anchor_arrow2d(s=20); +module anchor_arrow2d(s=15, color=[0.333,0.333,1], $tags="anchor-arrow") { + noop() color(color) stroke([[0,0],[0,s]], width=s/10, endcap1="butt", endcap2="arrow2"); +} + + + + + + +// Module: expose_anchors() +// Usage: +// expose_anchors(opacity) {child1() show_anchors(); child2() show_anchors(); ...} +// Description: +// Used in combination with show_anchors() to display an object in transparent gray with its anchors in solid color. +// Children will appear transparent and any anchor arrows drawn with will appear in solid color. +// Arguments: +// opacity = The opacity of the children. 0.0 is invisible, 1.0 is opaque. Default: 0.2 +// Example(FlatSpin,VPD=333): +// expose_anchors() cube(50, center=true) show_anchors(); +module expose_anchors(opacity=0.2) { + show("anchor-arrow") + children(); + hide("anchor-arrow") + color(is_undef($color)? [0,0,0] : + is_string($color)? $color : + point3d($color), opacity) + children(); +} + + + + +// Module: frame_ref() +// Usage: +// frame_ref(s, opacity); +// Description: +// Displays X,Y,Z axis arrows in red, green, and blue respectively. +// Arguments: +// s = Length of the arrows. +// opacity = The opacity of the arrows. 0.0 is invisible, 1.0 is opaque. Default: 1.0 +// Examples: +// frame_ref(25); +// frame_ref(30, opacity=0.5); +module frame_ref(s=15, opacity=1) { + cube(0.01, center=true) { + attach([1,0,0]) anchor_arrow(s=s, flag=false, color=[1.0, 0.3, 0.3, opacity]); + attach([0,1,0]) anchor_arrow(s=s, flag=false, color=[0.3, 1.0, 0.3, opacity]); + attach([0,0,1]) anchor_arrow(s=s, flag=false, color=[0.3, 0.3, 1.0, opacity]); + children(); + } +} + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/debug.scad b/debug.scad deleted file mode 100644 index 8f25187..0000000 --- a/debug.scad +++ /dev/null @@ -1,350 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// LibFile: debug.scad -// Helpers to make debugging OpenScad code easier. -// Includes: -// include -////////////////////////////////////////////////////////////////////// - - - -// Function: standard_anchors() -// Usage: -// anchs = standard_anchors([two_d]); -// Description: -// Return the vectors for all standard anchors. -// Arguments: -// two_d = If true, returns only the anchors where the Z component is 0. Default: false -function standard_anchors(two_d=false) = [ - for ( - zv = [ - if (!two_d) TOP, - CENTER, - if (!two_d) BOTTOM - ], - yv = [FRONT, CENTER, BACK], - xv = [LEFT, CENTER, RIGHT] - ) xv+yv+zv -]; - - - -// Module: anchor_arrow() -// Usage: -// anchor_arrow([s], [color], [flag]); -// Description: -// Show an anchor orientation arrow. By default, tagged with the name "anchor-arrow". -// Arguments: -// s = Length of the arrows. Default: `10` -// color = Color of the arrow. Default: `[0.333, 0.333, 1]` -// flag = If true, draw the orientation flag on the arrowhead. Default: true -// Example: -// anchor_arrow(s=20); -module anchor_arrow(s=10, color=[0.333,0.333,1], flag=true, $tags="anchor-arrow") { - $fn=12; - recolor("gray") spheroid(d=s/6) { - attach(CENTER,BOT) recolor(color) cyl(h=s*2/3, d=s/15) { - attach(TOP,BOT) cyl(h=s/3, d1=s/5, d2=0) { - if(flag) { - position(BOT) - recolor([1,0.5,0.5]) - cuboid([s/100, s/6, s/4], anchor=FRONT+BOT); - } - children(); - } - } - } -} - - - -// Module: anchor_arrow2d() -// Usage: -// anchor_arrow2d([s], [color], [flag]); -// Description: -// Show an anchor orientation arrow. -// Arguments: -// s = Length of the arrows. -// color = Color of the arrow. -// Example: -// anchor_arrow2d(s=20); -module anchor_arrow2d(s=15, color=[0.333,0.333,1], $tags="anchor-arrow") { - noop() color(color) stroke([[0,0],[0,s]], width=s/10, endcap1="butt", endcap2="arrow2"); -} - - - -// Module: expose_anchors() -// Usage: -// expose_anchors(opacity) {...} -// Description: -// Makes the children transparent gray, while showing any anchor arrows that may exist. -// Arguments: -// opacity = The opacity of the arrow. 0.0 is invisible, 1.0 is opaque. Default: 0.2 -// Example(FlatSpin,VPD=333): -// expose_anchors() cube(50, center=true) show_anchors(); -module expose_anchors(opacity=0.2) { - show("anchor-arrow") - children(); - hide("anchor-arrow") - color(is_undef($color)? [0,0,0] : - is_string($color)? $color : - point3d($color), opacity) - children(); -} - - -// Module: show_anchors() -// Usage: -// ... show_anchors([s], [std=], [custom=]); -// Description: -// Show all standard anchors for the parent object. -// Arguments: -// s = Length of anchor arrows. -// --- -// std = If true (default), show standard anchors. -// custom = If true (default), show custom anchors. -// Example(FlatSpin,VPD=333): -// cube(50, center=true) show_anchors(); -module show_anchors(s=10, std=true, custom=true) { - check = assert($parent_geom != undef) 1; - two_d = _attach_geom_2d($parent_geom); - if (std) { - for (anchor=standard_anchors(two_d=two_d)) { - if(two_d) { - attach(anchor) anchor_arrow2d(s); - } else { - attach(anchor) anchor_arrow(s); - } - } - } - if (custom) { - for (anchor=last($parent_geom)) { - attach(anchor[0]) { - if(two_d) { - anchor_arrow2d(s, color="cyan"); - } else { - anchor_arrow(s, color="cyan"); - } - color("black") - noop($tags="anchor-arrow") { - xrot(two_d? 0 : 90) { - back(s/3) { - yrot_copies(n=2) - up(s/30) { - linear_extrude(height=0.01, convexity=12, center=true) { - text(text=anchor[0], size=s/4, halign="center", valign="center"); - } - } - } - } - } - color([1, 1, 1, 0.4]) - noop($tags="anchor-arrow") { - xrot(two_d? 0 : 90) { - back(s/3) { - zcopies(s/21) cube([s/4.5*len(anchor[0]), s/3, 0.01], center=true); - } - } - } - } - } - } - children(); -} - - - -// Module: frame_ref() -// Usage: -// frame_ref(s, opacity); -// Description: -// Displays X,Y,Z axis arrows in red, green, and blue respectively. -// Arguments: -// s = Length of the arrows. -// opacity = The opacity of the arrows. 0.0 is invisible, 1.0 is opaque. Default: 1.0 -// Examples: -// frame_ref(25); -// frame_ref(30, opacity=0.5); -module frame_ref(s=15, opacity=1) { - cube(0.01, center=true) { - attach([1,0,0]) anchor_arrow(s=s, flag=false, color=[1.0, 0.3, 0.3, opacity]); - attach([0,1,0]) anchor_arrow(s=s, flag=false, color=[0.3, 1.0, 0.3, opacity]); - attach([0,0,1]) anchor_arrow(s=s, flag=false, color=[0.3, 0.3, 1.0, opacity]); - children(); - } -} - - -// Module: ruler() -// Usage: -// ruler(length, width, [thickness=], [depth=], [labels=], [pipscale=], [maxscale=], [colors=], [alpha=], [unit=], [inch=]); -// Description: -// Creates a ruler for checking dimensions of the model -// Arguments: -// length = length of the ruler. Default 100 -// width = width of the ruler. Default: size of the largest unit division -// --- -// thickness = thickness of the ruler. Default: 1 -// depth = the depth of mark subdivisions. Default: 3 -// labels = draw numeric labels for depths where labels are larger than 1. Default: false -// pipscale = width scale of the pips relative to the next size up. Default: 1/3 -// maxscale = log10 of the maximum width divisions to display. Default: based on input length -// colors = colors to use for the ruler, a list of two values. Default: `["black","white"]` -// alpha = transparency value. Default: 1.0 -// unit = unit to mark. Scales the ruler marks to a different length. Default: 1 -// inch = set to true for a ruler scaled to inches (assuming base dimension is mm). Default: false -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `LEFT+BACK+TOP` -// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` -// orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` -// Examples(2D,Big): -// ruler(100,depth=3); -// ruler(100,depth=3,labels=true); -// ruler(27); -// ruler(27,maxscale=0); -// ruler(100,pipscale=3/4,depth=2); -// ruler(100,width=2,depth=2); -// Example(2D,Big): Metric vs Imperial -// ruler(12,width=50,inch=true,labels=true,maxscale=0); -// fwd(50)ruler(300,width=50,labels=true); -module ruler(length=100, width, thickness=1, depth=3, labels=false, pipscale=1/3, maxscale, colors=["black","white"], alpha=1.0, unit=1, inch=false, anchor=LEFT+BACK+TOP, spin=0, orient=UP) -{ - inchfactor = 25.4; - assert(depth<=5, "Cannot render scales smaller than depth=5"); - assert(len(colors)==2, "colors must contain a list of exactly two colors."); - length = inch ? inchfactor * length : length; - unit = inch ? inchfactor*unit : unit; - maxscale = is_def(maxscale)? maxscale : floor(log(length/unit-EPSILON)); - scales = unit * [for(logsize = [maxscale:-1:maxscale-depth+1]) pow(10,logsize)]; - widthfactor = (1-pipscale) / (1-pow(pipscale,depth)); - width = default(width, scales[0]); - widths = width * widthfactor * [for(logsize = [0:-1:-depth+1]) pow(pipscale,-logsize)]; - offsets = concat([0],cumsum(widths)); - attachable(anchor,spin,orient, size=[length,width,thickness]) { - translate([-length/2, -width/2, 0]) - for(i=[0:1:len(scales)-1]) { - count = ceil(length/scales[i]); - fontsize = 0.5*min(widths[i], scales[i]/ceil(log(count*scales[i]/unit))); - back(offsets[i]) { - xcopies(scales[i], n=count, sp=[0,0,0]) union() { - actlen = ($idx0 ? quantup(widths[i],1/1024) : widths[i]; // What is the i>0 test supposed to do here? - cube([quantup(actlen,1/1024),quantup(w,1/1024),thickness], anchor=FRONT+LEFT); - } - mark = - i == 0 && $idx % 10 == 0 && $idx != 0 ? 0 : - i == 0 && $idx % 10 == 9 && $idx != count-1 ? 1 : - $idx % 10 == 4 ? 1 : - $idx % 10 == 5 ? 0 : -1; - flip = 1-mark*2; - if (mark >= 0) { - marklength = min(widths[i]/2, scales[i]*2); - markwidth = marklength*0.4; - translate([mark*scales[i], widths[i], 0]) { - color(colors[1-$idx%2], alpha=alpha) { - linear_extrude(height=thickness+scales[i]/100, convexity=2, center=true) { - polygon(scale([flip*markwidth, marklength],p=[[0,0], [1, -1], [0,-0.9]])); - } - } - } - } - if (labels && scales[i]/unit+EPSILON >= 1) { - color(colors[($idx+1)%2], alpha=alpha) { - linear_extrude(height=thickness+scales[i]/100, convexity=2, center=true) { - back(scales[i]*.02) { - text(text=str( $idx * scales[i] / unit), size=fontsize, halign="left", valign="baseline"); - } - } - } - } - - } - } - } - children(); - } -} - - -// Function: mod_indent() -// Usage: -// str = mod_indent([indent]); -// Description: -// Returns a string that is the total indentation for the module level you are at. -// Arguments: -// indent = The string to indent each level by. Default: " " (Two spaces) -// Example: -// x = echo(str(mod_indent(), parent_module(0))); -function mod_indent(indent=" ") = - str_join([for (i=[1:1:$parent_modules-1]) indent]); - - -// Function: mod_trace() -// Usage: -// str = mod_trace([levs], [indent=], [modsep=]); -// Description: -// Returns a string that shows the current module and its parents, indented for each unprinted parent module. -// Arguments: -// levs = This is the number of levels to print the names of. Prints the N most nested module names. Default: 2 -// --- -// indent = The string to indent each level by. Default: " " (Two spaces) -// modsep = Multiple module names will be separated by this string. Default: "->" -// Example: -// x = echo(mod_trace()); -function mod_trace(levs=2, indent=" ", modsep="->") = - str( - str_join([for (i=[1:1:$parent_modules+1-levs]) indent]), - str_join([for (i=[min(levs-1,$parent_modules-1):-1:0]) parent_module(i)], modsep) - ); - - -// Function&Module: echo_matrix() -// Usage: -// echo_matrix(M, [description=], [sig=], [eps=]); -// dummy = echo_matrix(M, [description=], [sig=], [eps=]), -// Description: -// Display a numerical matrix in a readable columnar format with `sig` significant -// digits. Values smaller than eps display as zero. If you give a description -// it is displayed at the top. -function echo_matrix(M,description,sig=4,eps=1e-9) = - let( - horiz_line = chr(8213), - matstr = matrix_strings(M,sig=sig,eps=eps), - separator = str_join(repeat(horiz_line,10)), - dummy=echo(str(separator," ",is_def(description) ? description : "")) - [for(row=matstr) echo(row)] - ) - echo(separator); - -module echo_matrix(M,description,sig=4,eps=1e-9) -{ - dummy = echo_matrix(M,description,sig,eps); -} - -// Function: random_polygon() -// Usage: -// points = random_polygon(n, size, [seed]); -// See Also: random_points(), gaussian_random_points(), spherical_random_points() -// Topics: Random, Polygon -// Description: -// Generate the `n` vertices of a random counter-clockwise simple 2d polygon -// inside a circle centered at the origin with radius `size`. -// Arguments: -// n = number of vertices of the polygon. Default: 3 -// size = the radius of a circle centered at the origin containing the polygon. Default: 1 -// seed = an optional seed for the random generation. -function random_polygon(n=3,size=1, seed) = - assert( is_int(n) && n>2, "Improper number of polygon vertices.") - assert( is_num(size) && size>0, "Improper size.") - let( - seed = is_undef(seed) ? rands(0,1,1)[0] : seed, - cumm = cumsum(rands(0.1,10,n+1,seed)), - angs = 360*cumm/cumm[n-1], - rads = rands(.01,size,n,seed+cumm[0]) - ) - [for(i=count(n)) rads[i]*[cos(angs[i]), sin(angs[i])] ]; - - - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/geometry.scad b/geometry.scad index 7ef4bc0..8832589 100644 --- a/geometry.scad +++ b/geometry.scad @@ -2160,5 +2160,66 @@ function _support_diff(p1,p2,d) = p1[search(max(p1d),p1d,1)[0]] - p2[search(min(p2d),p2d,1)[0]]; +// Section: Rotation Decoding + +// Function: rot_decode() +// Usage: +// info = rot_decode(rotation,[long]); // Returns: [angle,axis,cp,translation] +// Topics: Affine, Matrices, Transforms +// Description: +// Given an input 3D rigid transformation operator (one composed of just rotations and translations) represented +// as a 4x4 matrix, compute the rotation and translation parameters of the operator. Returns a list of the +// four parameters, the angle, in the interval [0,180], the rotation axis as a unit vector, a centerpoint for +// the rotation, and a translation. If you set `parms = rot_decode(rotation)` then the transformation can be +// reconstructed from parms as `move(parms[3]) * rot(a=parms[0],v=parms[1],cp=parms[2])`. This decomposition +// makes it possible to perform interpolation. If you construct a transformation using `rot` the decoding +// may flip the axis (if you gave an angle outside of [0,180]). The returned axis will be a unit vector, and +// the centerpoint lies on the plane through the origin that is perpendicular to the axis. It may be different +// than the centerpoint you used to construct the transformation. +// . +// If you set `long` to true then return the reversed rotation, with the angle in [180,360]. +// Arguments: +// rotation = rigid transformation to decode +// long = if true return the "long way" around, with the angle in [180,360]. Default: false +// Example: +// info = rot_decode(rot(45)); +// // Returns: [45, [0,0,1], [0,0,0], [0,0,0]] +// Example: +// info = rot_decode(rot(a=37, v=[1,2,3], cp=[4,3,-7]))); +// // Returns: [37, [0.26, 0.53, 0.80], [4.8, 4.6, -4.6], [0,0,0]] +// Example: +// info = rot_decode(left(12)*xrot(-33)); +// // Returns: [33, [-1,0,0], [0,0,0], [-12,0,0]] +// Example: +// info = rot_decode(translate([3,4,5])); +// // Returns: [0, [0,0,1], [0,0,0], [3,4,5]] +function rot_decode(M,long=false) = + assert(is_matrix(M,4,4) && approx(M[3],[0,0,0,1]), "Input matrix must be a 4x4 matrix representing a 3d transformation") + let(R = submatrix(M,[0:2],[0:2])) + assert(approx(det3(R),1) && approx(norm_fro(R * transpose(R)-ident(3)),0),"Input matrix is not a rotation") + let( + translation = [for(row=[0:2]) M[row][3]], // translation vector + largest = max_index([R[0][0], R[1][1], R[2][2]]), + axis_matrix = R + transpose(R) - (matrix_trace(R)-1)*ident(3), // Each row is on the rotational axis + // Construct quaternion q = c * [x sin(theta/2), y sin(theta/2), z sin(theta/2), cos(theta/2)] + q_im = axis_matrix[largest], + q_re = R[(largest+2)%3][(largest+1)%3] - R[(largest+1)%3][(largest+2)%3], + c_sin = norm(q_im), // c * sin(theta/2) for some c + c_cos = abs(q_re) // c * cos(theta/2) + ) + approx(c_sin,0) ? [0,[0,0,1],[0,0,0],translation] : + let( + angle = 2*atan2(c_sin, c_cos), // This is supposed to be more accurate than acos or asin + axis = (q_re>=0 ? 1:-1)*q_im/c_sin, + tproj = translation - (translation*axis)*axis, // Translation perpendicular to axis determines centerpoint + cp = (tproj + cross(axis,tproj)*c_cos/c_sin)/2 + ) + [long ? 360-angle:angle, + long? -axis : axis, + cp, + (translation*axis)*axis]; + + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/math.scad b/math.scad index fe3beae..0589425 100644 --- a/math.scad +++ b/math.scad @@ -588,29 +588,31 @@ function spherical_random_points(n, radius=1, seed) = - -// Function: log_rands() +// Function: random_polygon() // Usage: -// num = log_rands(minval, maxval, factor, [N], [seed]); +// points = random_polygon(n, size, [seed]); +// See Also: random_points(), gaussian_random_points(), spherical_random_points() +// Topics: Random, Polygon // Description: -// Returns a single random number, with a logarithmic distribution. +// Generate the `n` vertices of a random counter-clockwise simple 2d polygon +// inside a circle centered at the origin with radius `size`. // Arguments: -// minval = Minimum value to return. -// maxval = Maximum value to return. `minval` <= X < `maxval`. -// factor = Log factor to use. Values of X are returned `factor` times more often than X+1. -// N = Number of random numbers to return. Default: 1 -// seed = If given, sets the random number seed. -function log_rands(minval, maxval, factor, N=1, seed=undef) = - assert( is_finite(minval+maxval+N) - && (is_undef(seed) || is_finite(seed) ) - && factor>0, - "Input must be finite numbers. `factor` should be greater than zero.") - assert(maxval >= minval, "maxval cannot be smaller than minval") - let( - minv = 1-1/pow(factor,minval), - maxv = 1-1/pow(factor,maxval), - nums = is_undef(seed)? rands(minv, maxv, N) : rands(minv, maxv, N, seed) - ) [for (num=nums) -ln(1-num)/ln(factor)]; +// n = number of vertices of the polygon. Default: 3 +// size = the radius of a circle centered at the origin containing the polygon. Default: 1 +// seed = an optional seed for the random generation. +function random_polygon(n=3,size=1, seed) = + assert( is_int(n) && n>2, "Improper number of polygon vertices.") + assert( is_num(size) && size>0, "Improper size.") + let( + seed = is_undef(seed) ? rands(0,1,1)[0] : seed, + cumm = cumsum(rands(0.1,10,n+1,seed)), + angs = 360*cumm/cumm[n-1], + rads = rands(.01,size,n,seed+cumm[0]) + ) + [for(i=count(n)) rads[i]*[cos(angs[i]), sin(angs[i])] ]; + + + @@ -896,6 +898,38 @@ function convolve(p,q) = // Section: Matrix math +// Function: ident() +// Usage: +// mat = ident(n); +// Topics: Affine, Matrices +// Description: +// Create an `n` by `n` square identity matrix. +// Arguments: +// n = The size of the identity matrix square, `n` by `n`. +// Example: +// mat = ident(3); +// // Returns: +// // [ +// // [1, 0, 0], +// // [0, 1, 0], +// // [0, 0, 1] +// // ] +// Example: +// mat = ident(4); +// // Returns: +// // [ +// // [1, 0, 0, 0], +// // [0, 1, 0, 0], +// // [0, 0, 1, 0], +// // [0, 0, 0, 1] +// // ] +function ident(n) = [ + for (i = [0:1:n-1]) [ + for (j = [0:1:n-1]) (i==j)? 1 : 0 + ] +]; + + // Function: linear_solve() // Usage: // solv = linear_solve(A,b) diff --git a/shapes3d.scad b/shapes3d.scad index ab01138..815c15e 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -2457,4 +2457,100 @@ function heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04 ) reorient(anchor,spin,orient, vnf=vnf, p=vnf); + +// Module: ruler() +// Usage: +// ruler(length, width, [thickness=], [depth=], [labels=], [pipscale=], [maxscale=], [colors=], [alpha=], [unit=], [inch=]); +// Description: +// Creates a ruler for checking dimensions of the model +// Arguments: +// length = length of the ruler. Default 100 +// width = width of the ruler. Default: size of the largest unit division +// --- +// thickness = thickness of the ruler. Default: 1 +// depth = the depth of mark subdivisions. Default: 3 +// labels = draw numeric labels for depths where labels are larger than 1. Default: false +// pipscale = width scale of the pips relative to the next size up. Default: 1/3 +// maxscale = log10 of the maximum width divisions to display. Default: based on input length +// colors = colors to use for the ruler, a list of two values. Default: `["black","white"]` +// alpha = transparency value. Default: 1.0 +// unit = unit to mark. Scales the ruler marks to a different length. Default: 1 +// inch = set to true for a ruler scaled to inches (assuming base dimension is mm). Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `LEFT+BACK+TOP` +// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// Examples(2D,Big): +// ruler(100,depth=3); +// ruler(100,depth=3,labels=true); +// ruler(27); +// ruler(27,maxscale=0); +// ruler(100,pipscale=3/4,depth=2); +// ruler(100,width=2,depth=2); +// Example(2D,Big): Metric vs Imperial +// ruler(12,width=50,inch=true,labels=true,maxscale=0); +// fwd(50)ruler(300,width=50,labels=true); +module ruler(length=100, width, thickness=1, depth=3, labels=false, pipscale=1/3, maxscale, + colors=["black","white"], alpha=1.0, unit=1, inch=false, anchor=LEFT+BACK+TOP, spin=0, orient=UP) +{ + inchfactor = 25.4; + assert(depth<=5, "Cannot render scales smaller than depth=5"); + assert(len(colors)==2, "colors must contain a list of exactly two colors."); + length = inch ? inchfactor * length : length; + unit = inch ? inchfactor*unit : unit; + maxscale = is_def(maxscale)? maxscale : floor(log(length/unit-EPSILON)); + scales = unit * [for(logsize = [maxscale:-1:maxscale-depth+1]) pow(10,logsize)]; + widthfactor = (1-pipscale) / (1-pow(pipscale,depth)); + width = default(width, scales[0]); + widths = width * widthfactor * [for(logsize = [0:-1:-depth+1]) pow(pipscale,-logsize)]; + offsets = concat([0],cumsum(widths)); + attachable(anchor,spin,orient, size=[length,width,thickness]) { + translate([-length/2, -width/2, 0]) + for(i=[0:1:len(scales)-1]) { + count = ceil(length/scales[i]); + fontsize = 0.5*min(widths[i], scales[i]/ceil(log(count*scales[i]/unit))); + back(offsets[i]) { + xcopies(scales[i], n=count, sp=[0,0,0]) union() { + actlen = ($idx0 ? quantup(widths[i],1/1024) : widths[i]; // What is the i>0 test supposed to do here? + cube([quantup(actlen,1/1024),quantup(w,1/1024),thickness], anchor=FRONT+LEFT); + } + mark = + i == 0 && $idx % 10 == 0 && $idx != 0 ? 0 : + i == 0 && $idx % 10 == 9 && $idx != count-1 ? 1 : + $idx % 10 == 4 ? 1 : + $idx % 10 == 5 ? 0 : -1; + flip = 1-mark*2; + if (mark >= 0) { + marklength = min(widths[i]/2, scales[i]*2); + markwidth = marklength*0.4; + translate([mark*scales[i], widths[i], 0]) { + color(colors[1-$idx%2], alpha=alpha) { + linear_extrude(height=thickness+scales[i]/100, convexity=2, center=true) { + polygon(scale([flip*markwidth, marklength],p=[[0,0], [1, -1], [0,-0.9]])); + } + } + } + } + if (labels && scales[i]/unit+EPSILON >= 1) { + color(colors[($idx+1)%2], alpha=alpha) { + linear_extrude(height=thickness+scales[i]/100, convexity=2, center=true) { + back(scales[i]*.02) { + text(text=str( $idx * scales[i] / unit), size=fontsize, halign="left", valign="baseline"); + } + } + } + } + + } + } + } + children(); + } +} + + + + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/skin.scad b/skin.scad index 4f1326d..65e0df0 100644 --- a/skin.scad +++ b/skin.scad @@ -1188,6 +1188,220 @@ function slice_profiles(profiles,slices,closed=false) = concat(slicelist, closed?[]:[profiles[len(profiles)-1]]); + +function _closest_angle(alpha,beta) = + is_vector(beta) ? [for(entry=beta) _closest_angle(alpha,entry)] + : beta-alpha > 180 ? beta - ceil((beta-alpha-180)/360) * 360 + : beta-alpha < -180 ? beta + ceil((alpha-beta-180)/360) * 360 + : beta; + + +// Smooth data with N point moving average. If angle=true handles data as angles. +// If closed=true assumes last point is adjacent to the first one. +// If closed=false pads data with left/right value (probably wrong behavior...should do linear interp) +function _smooth(data,len,closed=false,angle=false) = + let( halfwidth = floor(len/2), + result = closed ? [for(i=idx(data)) + let( + window = angle ? _closest_angle(data[i],select(data,i-halfwidth,i+halfwidth)) + : select(data,i-halfwidth,i+halfwidth) + ) + mean(window)] + : [for(i=idx(data)) + let( + window = select(data,max(i-halfwidth,0),min(i+halfwidth,len(data)-1)), + left = i-halfwidth<0, + pad = left ? data[0] : last(data) + ) + sum(window)+pad*(len-len(window))] / len + ) + result; + +// Function: rot_resample() +// Usage: +// rlist = rot_resample(rotlist, N, [method], [twist], [scale], [smoothlen], [long], [turns], [closed]) +// Description: +// Takes as input a list of rotation matrices in 3d. Produces as output a resampled +// list of rotation operators (4x4 matrixes) suitable for use with sweep(). You can optionally apply twist to +// the output with the twist parameter, which is either a scalar to apply a uniform +// overall twist, or a vector to apply twist non-uniformly. Similarly you can apply +// scaling either overall or with a vector. The smoothlen parameter applies smoothing +// to the twist and scaling to prevent abrupt changes. This is done by a moving average +// of the smoothing or scaling values. The default of 1 means no smoothing. The long parameter causes +// the interpolation to be done the "long" way around the rotation instead of the short way. +// Note that the rotation matrix cannot distinguish which way you rotate, only the place you +// end after rotation. Another ambiguity arises if your rotation is more than 360 degrees. +// You can add turns with the turns parameter, so giving turns=1 will add 360 degrees to the +// rotation so it completes one full turn plus the additional rotation given my the transform. +// You can give long as a scalar or as a vector. Finally if closed is true then the +// resampling will connect back to the beginning. +// . +// The default is to resample based on the length of the arc defined by each rotation operator. This produces +// uniform sampling over all of the transformations. It requires that each rotation has nonzero length. +// In this case N specifies the total number of samples. If you set method to "count" then N you get +// N samples for each transform. You can set N to a vector to vary the samples at each step. +// Arguments: +// rotlist = list of rotation operators in 3d to resample +// N = Number of rotations to produce as output when method is "length" or number for each transformation if method is "count". Can be a vector when method is "count" +// -- +// method = sampling method, either "length" or "count" +// twist = scalar or vector giving twist to add overall or at each rotation. Default: none +// scale = scalar or vector giving scale factor to add overall or at each rotation. Default: none +// smoothlen = amount of smoothing to apply to scaling and twist. Should be an odd integer. Default: 1 +// long = resample the "long way" around the rotation, a boolean or list of booleans. Default: false +// turns = add extra turns. If a scalar adds the turns to every rotation, or give a vector. Default: 0 +// closed = if true then the rotation list is treated as closed. Default: false +// Example: Resampling the arc from a compound rotation with translations thrown in. +// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25); +// sweep(circle(r=1,$fn=3), tran); +// Example: Applying a scale factor +// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25, scale=2); +// sweep(circle(r=1,$fn=3), tran); +// Example: Applying twist +// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25, twist=60); +// sweep(circle(r=1,$fn=3), tran); +// Example: Going the long way +// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], N=25, long=true); +// sweep(circle(r=1,$fn=3), tran); +// Example: Getting transformations from turtle3d +// include +// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,170],transforms=true); +// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40)); +// Example: If you specify a larger angle in turtle you need to use the long argument +// include +// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270],transforms=true); +// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40,long=true)); +// Example: And if the angle is over 360 you need to add turns to get the right result. Note long is false when the remaining angle after subtracting full turns is below 180: +// include +// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,90+360],transforms=true); +// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40,long=false,turns=1)); +// Example: Here the remaining angle is 270, so long must be set to true +// include +// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270+360],transforms=true); +// sweep(circle(r=1,$fn=3),rot_resample(tran, N=40,long=true,turns=1)); +// Example: Note the visible line at the scale transition +// include +// tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true); +// rtran = rot_resample(tran,200,scale=[1,6]); +// sweep(circle(1,$fn=32),rtran); +// Example: Observe how using a large smoothlen value eases that transition +// include +// tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true); +// rtran = rot_resample(tran,200,scale=[1,6],smoothlen=17); +// sweep(circle(1,$fn=32),rtran); +// Example: A similar issues can arise with twist, where a "line" is visible at the transition +// include +// tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]); +// rtran = rot_resample(tran,100,twist=[0,60],smoothlen=1); +// sweep(subdivide_path(rect([3,3]),40),rtran); +// Example: Here's the smoothed twist transition +// include +// tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]); +// rtran = rot_resample(tran,100,twist=[0,60],smoothlen=17); +// sweep(subdivide_path(rect([3,3]),40),rtran); +// Example: toothed belt based on list-comprehension-demos example. This version has a smoothed twist transition. Try changing smoothlen to 1 to see the more abrupt transition that occurs without smoothing. +// include +// r_small = 19; // radius of small curve +// r_large = 46; // radius of large curve +// flat_length = 100; // length of flat belt section +// teeth=42; // number of teeth +// belt_width = 12; +// tooth_height = 9; +// belt_thickness = 3; +// angle = 180 - 2*atan((r_large-r_small)/flat_length); +// beltprofile = path3d(subdivide_path( +// square([belt_width, belt_thickness],anchor=FWD), +// 20)); +// beltrots = +// turtle3d(["arcsteps",1, +// "move", flat_length, +// "arcleft", r_small, angle, +// "move", flat_length, +// // Closing path will be interpolated +// // "arcleft", r_large, 360-angle +// ],transforms=true); +// beltpath = rot_resample(beltrots,teeth*4, +// twist=[180,0,-180,0], +// long=[false,false,false,true], +// smoothlen=15,closed=true); +// belt = [for(i=idx(beltpath)) +// let(tooth = floor((i+$t*4)/2)%2) +// apply(beltpath[i]* +// yscale(tooth +// ? tooth_height/belt_thickness +// : 1), +// beltprofile) +// ]; +// skin(belt,slices=0,closed=true); +function rot_resample(rotlist,N,twist,scale,smoothlen=1,long=false,turns=0,closed=false,method="length") = + assert(is_int(smoothlen) && smoothlen>0 && smoothlen%2==1, "smoothlen must be a positive odd integer") + assert(method=="length" || method=="count") + let(tcount = len(rotlist) + (closed?0:-1)) + assert(method=="count" || is_int(N), "N must be an integer when method is \"length\"") + assert(is_int(N) || is_vector(N,tcount), str("N must be scalar or vector with length ",tcount)) + let( + count = method=="length" ? (closed ? N+1 : N) + : (is_vector(N) ? sum(N) : tcount*N)+1 //(closed?0:1) + ) + assert(is_bool(long) || len(long)==tcount,str("Input long must be a scalar or have length ",tcount)) + let( + long = force_list(long,tcount), + turns = force_list(turns,tcount), + T = [for(i=[0:1:tcount-1]) rot_inverse(rotlist[i])*select(rotlist,i+1)], + parms = [for(i=idx(T)) + let(tparm = rot_decode(T[i],long[i])) + [tparm[0]+turns[i]*360,tparm[1],tparm[2],tparm[3]] + ], + radius = [for(i=idx(parms)) norm(parms[i][2])], + length = [for(i=idx(parms)) norm([norm(parms[i][3]), parms[i][0]/360*2*PI*radius[i]])] + ) + assert(method=="count" || all_positive(length), + "Rotation list includes a repeated entry or a rotation around the origin, not allowed when method=\"length\"") + let( + cumlen = [0, each cumsum(length)], + totlen = last(cumlen), + stepsize = totlen/(count-1), + samples = method=="count" + ? let( N = force_list(N,tcount)) + [for(n=N) lerpn(0,1,n,endpoint=false)] + :[for(i=idx(parms)) + let( + remainder = cumlen[i] % stepsize, + offset = remainder==0 ? 0 + : stepsize-remainder, + num = ceil((length[i]-offset)/stepsize) + ) + count(num,offset,stepsize)/length[i]], + twist = first_defined([twist,0]), + scale = first_defined([scale,1]), + needlast = !approx(last(last(samples)),1), + sampletwist = is_num(twist) ? lerpn(0,twist,count) + : let( + cumtwist = [0,each cumsum(twist)] + ) + [for(i=idx(parms)) each lerp(cumtwist[i],cumtwist[i+1],samples[i]), + if (needlast) last(cumtwist) + ], + samplescale = is_num(scale) ? lerp(1,scale,lerpn(0,1,count)) + : let( + cumscale = [1,each cumprod(scale)] + ) + [for(i=idx(parms)) each lerp(cumscale[i],cumscale[i+1],samples[i]), + if (needlast) last(cumscale)], + smoothtwist = _smooth(closed?select(sampletwist,0,-2):sampletwist,smoothlen,closed=closed,angle=true), + smoothscale = _smooth(samplescale,smoothlen,closed=closed), + interpolated = [ + for(i=idx(parms)) + each [for(u=samples[i]) rotlist[i] * move(u*parms[i][3]) * rot(a=u*parms[i][0],v=parms[i][1],cp=parms[i][2])], + if (needlast) last(rotlist) + ] + ) + [for(i=idx(interpolated,e=closed?-2:-1)) interpolated[i]*zrot(smoothtwist[i])*scale(smoothscale[i])]; + + + + + ////////////////////////////////////////////////////////////////// // // Minimum Distance Mapping using Dynamic Programming diff --git a/std.scad b/std.scad index cd2c542..8c9066d 100644 --- a/std.scad +++ b/std.scad @@ -34,7 +34,6 @@ include include include include -include // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_affine.scad b/tests/test_affine.scad index 15d6877..819d7e1 100644 --- a/tests/test_affine.scad +++ b/tests/test_affine.scad @@ -31,25 +31,6 @@ module test_is_2d_transform() { test_is_2d_transform(); -module test_is_affine() { - assert(is_affine(affine2d_scale([2,3]))); - assert(is_affine(affine3d_scale([2,3,4]))); - assert(!is_affine(affine3d_scale([2,3,4]),2)); - assert(is_affine(affine2d_scale([2,3]),2)); - assert(is_affine(affine3d_scale([2,3,4]),3)); - assert(!is_affine(affine2d_scale([2,3]),3)); -} -test_is_affine(); - - -module test_affine2d_to_3d() { - assert(affine2d_to_3d(affine2d_identity()) == affine3d_identity()); - assert(affine2d_to_3d(affine2d_translate([30,40])) == affine3d_translate([30,40,0])); - assert(affine2d_to_3d(affine2d_scale([3,4])) == affine3d_scale([3,4,1])); - assert(affine2d_to_3d(affine2d_zrot(30)) == affine3d_zrot(30)); -} -test_affine2d_to_3d(); - // 2D diff --git a/tests/test_debug.scad b/tests/test_debug.scad deleted file mode 100644 index b6351c5..0000000 --- a/tests/test_debug.scad +++ /dev/null @@ -1,11 +0,0 @@ -include <../std.scad> - - -module test_standard_anchors() { - assert_equal(standard_anchors(), [[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,-1],[0,-1,-1],[1,-1,-1],[-1,0,-1],[0,0,-1],[1,0,-1],[-1,1,-1],[0,1,-1],[1,1,-1]]); -} -test_standard_anchors(); - - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_math.scad b/tests/test_math.scad index f713490..757ea5e 100644 --- a/tests/test_math.scad +++ b/tests/test_math.scad @@ -372,18 +372,6 @@ module test_gaussian_rands() { test_gaussian_rands(); -module test_log_rands() { - nums1 = log_rands(0,100,10,1000,seed=2189); - nums2 = log_rands(0,100,10,1000,seed=2310); - nums3 = log_rands(0,100,10,1000,seed=2189); - assert_equal(len(nums1), 1000); - assert_equal(len(nums2), 1000); - assert_equal(len(nums3), 1000); - assert_equal(nums1, nums3); - assert(nums1!=nums2); -} -test_log_rands(); - module test_segs() { assert_equal(segs(50,$fn=8), 8); diff --git a/transforms.scad b/transforms.scad index 339388a..d75f06c 100644 --- a/transforms.scad +++ b/transforms.scad @@ -1260,4 +1260,84 @@ function skew(p, sxy=0, sxz=0, syx=0, syz=0, szx=0, szy=0, planar=false) = [for (l=p) skew(sxy=sxy, sxz=sxz, syx=syx, syz=syz, szx=szx, szy=szy, planar=planar, p=l)]; +// Section: Applying transformation matrices to + + +/// Internal Function: is_2d_transform() +/// Usage: +/// x = is_2d_transform(t); +/// Topics: Affine, Matrices, Transforms, Type Checking +/// See Also: is_affine(), is_matrix() +/// Description: +/// Checks if the input is a 3D transform that does not act on the z coordinate, except possibly +/// for a simple scaling of z. Note that an input which is only a zscale returns false. +/// Arguments: +/// t = The transformation matrix to check. +/// Example: +/// b = is_2d_transform(zrot(45)); // Returns: true +/// b = is_2d_transform(yrot(45)); // Returns: false +/// b = is_2d_transform(xrot(45)); // Returns: false +/// b = is_2d_transform(move([10,20,0])); // Returns: true +/// b = is_2d_transform(move([10,20,30])); // Returns: false +/// b = is_2d_transform(scale([2,3,4])); // Returns: true +function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][2]!=1 so scale() works + t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && + (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() + + + +// Function: apply() +// Usage: +// pts = apply(transform, points); +// Topics: Affine, Matrices, Transforms +// Description: +// Applies the specified transformation matrix to a point, pointlist, bezier patch or VNF. +// Both inputs can be 2D or 3D, and it is also allowed to supply 3D transformations with 2D +// data as long as the the only action on the z coordinate is a simple scaling. +// . +// If you construct your own matrices you can also use a transform that acts like a projection +// with fewer rows to produce lower dimensional output. +// Arguments: +// transform = The 2D or 3D transformation matrix to apply to the point/points. +// points = The point, pointlist, bezier patch, or VNF to apply the transformation to. +// Example(3D): +// path1 = path3d(circle(r=40)); +// tmat = xrot(45); +// path2 = apply(tmat, path1); +// #stroke(path1,closed=true); +// stroke(path2,closed=true); +// Example(2D): +// path1 = circle(r=40); +// tmat = translate([10,5]); +// path2 = apply(tmat, path1); +// #stroke(path1,closed=true); +// stroke(path2,closed=true); +// Example(2D): +// path1 = circle(r=40); +// tmat = rot(30) * back(15) * scale([1.5,0.5,1]); +// path2 = apply(tmat, path1); +// #stroke(path1,closed=true); +// stroke(path2,closed=true); +function apply(transform,points) = + points==[] ? [] : + is_vector(points) + ? /* Point */ apply(transform, [points])[0] : + is_list(points) && len(points)==2 && is_path(points[0],3) && is_list(points[1]) && is_vector(points[1][0]) + ? /* VNF */ [apply(transform, points[0]), points[1]] : + is_list(points) && is_list(points[0]) && is_vector(points[0][0]) + ? /* BezPatch */ [for (x=points) apply(transform,x)] : + let( + tdim = len(transform[0])-1, + datadim = len(points[0]), + outdim = min(datadim,len(transform)), + matrix = [for(i=[0:1:tdim]) [for(j=[0:1:outdim-1]) transform[j][i]]] + ) + tdim==datadim && (datadim==3 || datadim==2) ? [for(p=points) concat(p,1)] * matrix + : tdim == 3 && datadim == 2 ? + assert(is_2d_transform(transform), str("Transforms is 3d but points are 2d")) + [for(p=points) concat(p,[0,1])]*matrix + : assert(false, str("Unsupported combination: transform with dimension ",tdim,", data of dimension ",datadim)); + + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap