diff --git a/affine.scad b/affine.scad index 0c27e475..f5100226 100644 --- a/affine.scad +++ b/affine.scad @@ -882,64 +882,6 @@ function affine3d_rot_from_to(from, to) = ]; -// Function: affine3d_frame_map() -// Usage: -// map = affine3d_frame_map(v1, v2, v3, [reverse=]); -// map = affine3d_frame_map(x=VECTOR1, y=VECTOR2, [reverse=]); -// map = affine3d_frame_map(x=VECTOR1, z=VECTOR2, [reverse=]); -// map = affine3d_frame_map(y=VECTOR1, z=VECTOR2, [reverse=]); -// Topics: Affine, Matrices, Transforms, Rotation -// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() -// Description: -// Returns a transformation that maps one coordinate frame to another. You must specify two or -// three of `x`, `y`, and `z`. The specified axes are mapped to the vectors you supplied. If you -// give two inputs, the third vector is mapped to the appropriate normal to maintain a right hand -// coordinate system. If the vectors you give are orthogonal the result will be a rotation and the -// `reverse` parameter will supply the inverse map, which enables you to map two arbitrary -// coordinate systems to each other by using the canonical coordinate system as an intermediary. -// You cannot use the `reverse` option with non-orthogonal inputs. -// Arguments: -// x = Destination 3D vector for x axis. -// y = Destination 3D vector for y axis. -// z = Destination 3D vector for z axis. -// reverse = reverse direction of the map for orthogonal inputs. Default: false -// Example: -// T = affine3d_frame_map(x=[1,1,0], y=[-1,1,0]); // This map is just a rotation around the z axis -// Example: -// T = affine3d_frame_map(x=[1,0,0], y=[1,1,0]); // This map is not a rotation because x and y aren't orthogonal -// Example: -// // The next map sends [1,1,0] to [0,1,1] and [-1,1,0] to [0,-1,1] -// T = affine3d_frame_map(x=[0,1,1], y=[0,-1,1]) * affine3d_frame_map(x=[1,1,0], y=[-1,1,0],reverse=true); -function affine3d_frame_map(x,y,z, reverse=false) = - assert(num_defined([x,y,z])>=2, "Must define at least two inputs") - let( - xvalid = is_undef(x) || (is_vector(x) && len(x)==3), - yvalid = is_undef(y) || (is_vector(y) && len(y)==3), - zvalid = is_undef(z) || (is_vector(z) && len(z)==3) - ) - assert(xvalid,"Input x must be a length 3 vector") - assert(yvalid,"Input y must be a length 3 vector") - assert(zvalid,"Input z must be a length 3 vector") - let( - x = is_undef(x)? undef : unit(x,RIGHT), - y = is_undef(y)? undef : unit(y,BACK), - z = is_undef(z)? undef : unit(z,UP), - map = is_undef(x)? [cross(y,z), y, z] : - is_undef(y)? [x, cross(z,x), z] : - is_undef(z)? [x, y, cross(x,y)] : - [x, y, z] - ) - reverse? ( - let( - ocheck = ( - approx(map[0]*map[1],0) && - approx(map[0]*map[2],0) && - approx(map[1]*map[2],0) - ) - ) - assert(ocheck, "Inputs must be orthogonal when reverse==true") - [for (r=map) [for (c=r) c, 0], [0,0,0,1]] - ) : [for (r=transpose(map)) [for (c=r) c, 0], [0,0,0,1]]; diff --git a/common.scad b/common.scad index c172cbb4..29fdd306 100644 --- a/common.scad +++ b/common.scad @@ -566,7 +566,7 @@ function no_function(name) = // Description: // Asserts that the called module exists only as a function. // Example: -// function foo() = no_module(); +// module foo() { no_module(); } module no_module() { assert(false, str("You called ",parent_module(1),"() as a module but it is available only as a function")); } diff --git a/coords.scad b/coords.scad index 7f4630d7..ffd09738 100644 --- a/coords.scad +++ b/coords.scad @@ -224,7 +224,7 @@ function project_plane(plane,p) = y = unit(plane[1]-plane[0]), // y axis goes to point b x = unit(v-(v*y)*y) // x axis ) - affine3d_frame_map(x,y) * move(-plane[0]) + frame_map(x,y) * move(-plane[0]) : is_vector(plane,4) && is_undef(p) ? // no data, plane given in "plane" assert(_valid_plane(plane), "Plane is not valid") let( @@ -280,7 +280,7 @@ function lift_plane(plane, p) = y = unit(plane[1]-plane[0]), // y axis goes to point b x = unit(v-(v*y)*y) // x axis ) - move(plane[0]) * affine3d_frame_map(x,y,reverse=true) + move(plane[0]) * frame_map(x,y,reverse=true) : is_vector(plane,4) && is_undef(p) ? // no data, plane given in "plane" assert(_valid_plane(plane), "Plane is not valid") let( diff --git a/paths.scad b/paths.scad index 2adb204d..0eddd19a 100644 --- a/paths.scad +++ b/paths.scad @@ -1630,7 +1630,7 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals if(planar) { rot(from=[0,1],to=cutlist[i][3]) children(); } else { - multmatrix(affine3d_frame_map(x=cutlist[i][2], z=cutlist[i][3])) + frame_map(x=cutlist[i][2], z=cutlist[i][3]) children(); } } else { @@ -1777,9 +1777,9 @@ module path_text(path, text, font, size, thickness=1, lettersize, offset=0, reve ) move(pts[i][0]) - multmatrix(affine3d_frame_map(x=pts[i][2]-adjustment, - z=usetop ? undef : normpts[i], - y=usetop ? toppts[i] : undef)) + frame_map(x=pts[i][2]-adjustment, + z=usetop ? undef : normpts[i], + y=usetop ? toppts[i] : undef) up(offset-thickness/2) linear_extrude(height=thickness) left(lsize[0]/2)text(text[i], font=font, size=size); diff --git a/regions.scad b/regions.scad index 378576cc..3355bb39 100644 --- a/regions.scad +++ b/regions.scad @@ -482,6 +482,7 @@ function _offset_chamfer(center, points, delta) = function _shift_segment(segment, d) = + assert(!approx(segment[0],segment[1]),"Path has repeated points") move(d*line_normal(segment),segment); diff --git a/shapes2d.scad b/shapes2d.scad index 724bab93..97914332 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1,11 +1,22 @@ ////////////////////////////////////////////////////////////////////// // LibFile: shapes2d.scad -// Common useful 2D shapes. +// This file includes stroke(), which converts a path into a +// geometric object, like drawing with a pen. It even works on +// three-dimensional paths. You can make a dashed line or add arrow +// heads. The turtle() function provides a turtle graphics style +// approach for producing paths. You can create regular polygons +// with optional rounded corners and alignment features not +// available with circle(). The file also provides teardrop2d, +// which is useful for 3d printable holes. Lastly you can use the +// masks to produce edge treatments common in furniture from the +// simple roundover or cove molding to the more elaborate ogee. +// Many of the commands have module forms that produce geometry and +// function forms that produce a path. // Includes: // include ////////////////////////////////////////////////////////////////////// -// Section: 2D Drawing Helpers +// Section: Line Drawing // Module: stroke() // Usage: @@ -109,6 +120,16 @@ // Example: 3D Path with Joints and Endcaps // path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]]; // stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18); +function stroke( + path, width=1, closed=false, + endcaps, endcap1, endcap2, joints, plots, + endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, + endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, + endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, + endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, + trim, trim1, trim2, + convexity=10, hull=true +) = no_function("stroke"); module stroke( path, width=1, closed=false, endcaps, endcap1, endcap2, joints, plots, @@ -743,6 +764,7 @@ function _normal_segment(p1,p2) = // ); // koch=concat(["angle",60,"repeat",3],[concat(koch_unit(3),["left","left"])]); // polygon(turtle(koch)); +module turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) {no_module();} function turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) = let( state = is_vector(state) ? [[state],[1,0],90,0] : state ) repeat == 1? @@ -1055,8 +1077,7 @@ function oval(r, d, realign=false, circum=false, anchor=CENTER, spin=0) = ) reorient(anchor,spin, two_d=true, r=[rx,ry], p=pts); - -// Section: 2D N-Gons +// Section: Polygons // Function&Module: regular_ngon() // Usage: @@ -1377,10 +1398,6 @@ module octagon(r, d, or, od, ir, id, side, rounding=0, realign=false, align_tip, regular_ngon(n=8, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, align_tip=align_tip, align_side=align_side, anchor=anchor, spin=spin) children(); - -// Section: Other 2D Shapes - - // Function&Module: trapezoid() // Usage: As Module // trapezoid(h, w1, w2, [shift=], [rounding=], [chamfer=], ...); @@ -1486,6 +1503,136 @@ module trapezoid(h, w1, w2, angle, shift=0, chamfer=0, rounding=0, anchor=CENTER } + +// Function&Module: star() +// Usage: As Module +// star(n, r/or, ir, [realign=], [align_tip=], [align_pit=], ...); +// star(n, r/or, step=, ...); +// Usage: With Attachments +// star(n, r/or, ir, ...) { attachments } +// Usage: As Function +// path = star(n, r/or, ir, [realign=], [align_tip=], [align_pit=], ...); +// path = star(n, r/or, step=, ...); +// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable +// See Also: circle(), oval() +// Description: +// When called as a function, returns the path needed to create a star polygon with N points. +// When called as a module, creates a star polygon with N points. +// Arguments: +// n = The number of stellate tips on the star. +// r/or = The radius to the tips of the star. +// ir = The radius to the inner corners of the star. +// --- +// d/od = The diameter to the tips of the star. +// id = The diameter to the inner corners of the star. +// step = Calculates the radius of the inner star corners by virtually drawing a straight line `step` tips around the star. 2 <= step < n/2 +// realign = If false, a tip is aligned with the Y+ axis. If true, an inner corner is aligned with the Y+ axis. Default: false +// align_tip = If given as a 2D vector, rotates the whole shape so that the first star tip points in that direction. This occurs before spin. +// align_pit = If given as a 2D vector, rotates the whole shape so that the first inner corner is pointed towards that direction. This occurs before spin. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Extra Anchors: +// "tip0" ... "tip4" = Each tip has an anchor, pointing outwards. +// "pit0" ... "pit4" = The inside corner between each tip has an anchor, pointing outwards. +// "midpt0" ... "midpt4" = The center-point between each pair of tips has an anchor, pointing outwards. +// Examples(2D): +// star(n=5, r=50, ir=25); +// star(n=5, r=50, step=2); +// star(n=7, r=50, step=2); +// star(n=7, r=50, step=3); +// Example(2D): Realigned +// star(n=7, r=50, step=3, realign=true); +// Example(2D): Alignment by Tip +// star(n=5, ir=15, or=30, align_tip=BACK+RIGHT) +// attach("tip0", FWD) color("blue") +// stroke([[0,0],[0,7]], endcap2="arrow2"); +// Example(2D): Alignment by Pit +// star(n=5, ir=15, or=30, align_pit=BACK+RIGHT) +// attach("pit0", FWD) color("blue") +// stroke([[0,0],[0,7]], endcap2="arrow2"); +// Example(2D): Called as Function +// stroke(closed=true, star(n=5, r=50, ir=25)); +function star(n, r, ir, d, or, od, id, step, realign=false, align_tip, align_pit, anchor=CENTER, spin=0, _mat, _anchs) = + assert(is_undef(align_tip) || is_vector(align_tip)) + assert(is_undef(align_pit) || is_vector(align_pit)) + assert(is_undef(align_tip) || is_undef(align_pit), "Can only specify one of align_tip and align_pit") + let( + r = get_radius(r1=or, d1=od, r=r, d=d), + count = num_defined([ir,id,step]), + stepOK = is_undef(step) || (step>1 && step1 && stepdepth && _r2>depth, "Screw profile deeper than rod radius"); map_threads = right((_r1 + _r2) / 2) // Shift profile out to thread radius * affine3d_skew(sxz=(_r2-_r1)/l) // Skew correction for tapered threads - * affine3d_frame_map(x=[0,0,1], y=[1,0,0]) // Map profile to 3d, parallel to z axis + * frame_map(x=[0,0,1], y=[1,0,0]) // Map profile to 3d, parallel to z axis * scale(pitch); // scale profile by pitch hig_table = [ [-twist/2-0.0001, 0], diff --git a/transforms.scad b/transforms.scad index d3bcdfb4..67de0e4b 100644 --- a/transforms.scad +++ b/transforms.scad @@ -1,6 +1,14 @@ ////////////////////////////////////////////////////////////////////// // LibFile: transforms.scad -// Functions and modules for translation, rotation, reflection and skewing. +// Functions and modules that provide shortcuts for translation, +// rotation and mirror operations. Also provided are skew and frame_map +// which remaps the coordinate axes. The shortcuts can act on +// geometry, like the usual OpenSCAD rotate() and translate(). They +// also work as functions that operate on lists of points in various +// forms: paths, VNFS and bezier patches. Lastly, the function form +// of the shortcuts can return a matrix representing the operation +// the shortcut performs. The rotation and scaling shortcuts accept +// an optional centerpoint for the rotation or scaling operation. // Includes: // include ////////////////////////////////////////////////////////////////////// @@ -771,6 +779,8 @@ module xyzrot(a=0, p, cp) function xyzrot(a=0, p, cp) = rot(a=a, v=[1,1,1], cp=cp, p=p); + + ////////////////////////////////////////////////////////////////////// // Section: Scaling and Mirroring ////////////////////////////////////////////////////////////////////// @@ -1011,6 +1021,10 @@ function zscale(z=1, p, cp=0) = scale([1,1,z], cp=cp, p=p); +////////////////////////////////////////////////////////////////////// +// Section: Reflection (Mirroring) +////////////////////////////////////////////////////////////////////// + // Function&Module: mirror() // Usage: As Module // mirror(v) ... @@ -1435,9 +1449,87 @@ function yzflip(p, cp=0) = ////////////////////////////////////////////////////////////////////// -// Section: Skewing +// Section: Other Transformations ////////////////////////////////////////////////////////////////////// +// Function&Module: frame_map() +// Usage: As module +// frame_map(v1, v2, v3, [reverse=]) { ... } +// Usage: As function to remap points +// transformed = frame_map(v1, v2, v3, p=points, [reverse=]); +// Usage: As function to return a transformation matrix: +// map = frame_map(v1, v2, v3, [reverse=]); +// map = frame_map(x=VECTOR1, y=VECTOR2, [reverse=]); +// map = frame_map(x=VECTOR1, z=VECTOR2, [reverse=]); +// map = frame_map(y=VECTOR1, z=VECTOR2, [reverse=]); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() +// Description: +// Maps one coordinate frame to another. You must specify two or +// three of `x`, `y`, and `z`. The specified axes are mapped to the vectors you supplied, so if you +// specify x=[1,1] then the x axis will be mapped to the line y=x. If you +// give two inputs, the third vector is mapped to the appropriate normal to maintain a right hand +// coordinate system. If the vectors you give are orthogonal the result will be a rotation and the +// `reverse` parameter will supply the inverse map, which enables you to map two arbitrary +// coordinate systems to each other by using the canonical coordinate system as an intermediary. +// You cannot use the `reverse` option with non-orthogonal inputs. Note that only the direction +// of the specified vectors matters: the transformation will not apply scaling, though it can +// skew if your provide non-orthogonal axes. +// Arguments: +// x = Destination 3D vector for x axis. +// y = Destination 3D vector for y axis. +// z = Destination 3D vector for z axis. +// reverse = reverse direction of the map for orthogonal inputs. Default: false +// Example: Remap axes after linear extrusion +// frame_map(x=[0,1,0], y=[0,0,1]) linear_extrude(height=10) square(3); +// Example: This map is just a rotation around the z axis +// mat = frame_map(x=[1,1,0], y=[-1,1,0]); +// Example: This map is not a rotation because x and y aren't orthogonal +// mat = frame_map(x=[1,0,0], y=[1,1,0]); +// Example: This sends [1,1,0] to [0,1,1] and [-1,1,0] to [0,-1,1] +// mat = frame_map(x=[0,1,1], y=[0,-1,1]) * frame_map(x=[1,1,0], y=[-1,1,0],reverse=true); +function frame_map(x,y,z, p, reverse=false) = + is_def(p) + ? apply(frame_map(x,y,z,reverse=reverse), p) + : + assert(num_defined([x,y,z])>=2, "Must define at least two inputs") + let( + xvalid = is_undef(x) || (is_vector(x) && len(x)==3), + yvalid = is_undef(y) || (is_vector(y) && len(y)==3), + zvalid = is_undef(z) || (is_vector(z) && len(z)==3) + ) + assert(xvalid,"Input x must be a length 3 vector") + assert(yvalid,"Input y must be a length 3 vector") + assert(zvalid,"Input z must be a length 3 vector") + let( + x = is_undef(x)? undef : unit(x,RIGHT), + y = is_undef(y)? undef : unit(y,BACK), + z = is_undef(z)? undef : unit(z,UP), + map = is_undef(x)? [cross(y,z), y, z] : + is_undef(y)? [x, cross(z,x), z] : + is_undef(z)? [x, y, cross(x,y)] : + [x, y, z] + ) + reverse? ( + let( + ocheck = ( + approx(map[0]*map[1],0) && + approx(map[0]*map[2],0) && + approx(map[1]*map[2],0) + ) + ) + assert(ocheck, "Inputs must be orthogonal when reverse==true") + [for (r=map) [for (c=r) c, 0], [0,0,0,1]] + ) : [for (r=transpose(map)) [for (c=r) c, 0], [0,0,0,1]]; + + +module frame_map(x,y,z,p,reverse=false) +{ + assert(is_undef(p), "Module form `frame_map()` does not accept p= argument."); + multmatrix(frame_map(x,y,z,reverse=reverse)) + children(); +} + // Function&Module: skew() // Usage: As Module diff --git a/turtle3d.scad b/turtle3d.scad index 200c6e28..0b4c9438 100644 --- a/turtle3d.scad +++ b/turtle3d.scad @@ -433,7 +433,7 @@ function turtle3d(commands, state=RIGHT, transforms=false, full_state=false, rep state = is_matrix(state,4,4) ? [[state],[yrot(90)],1,90,0] : is_vector(state,3) ? let( updir = UP - (UP * state) * state / (state*state) ) - [[affine3d_frame_map(x=state, z=approx(norm(updir),0) ? FWD : updir)], [yrot(90)],1, 90, 0] + [[frame_map(x=state, z=approx(norm(updir),0) ? FWD : updir)], [yrot(90)],1, 90, 0] : assert(_turtle3d_state_valid(state), "Supplied state is not valid") state, finalstate = _turtle3d_repeat(commands, state, repeat)