////////////////////////////////////////////////////////////////////// // LibFile: skin.scad // Functions to skin arbitrary 2D profiles/paths in 3-space. // To use, add the following line to the beginning of your file: // ``` // include // include // ``` // Derived from list-comprehension-demos skin(): // - https://github.com/openscad/list-comprehension-demos/blob/master/skin.scad ////////////////////////////////////////////////////////////////////// include // Section: Skinning // Function&Module: skin() // Usage: As Module // skin(profiles, [closed], [matching]); // Usage: As Function // vnf = skin(profiles, [closed], [caps], [matching]); // Description // Given a list of two or more 2D path `profiles` that have been moved and/or rotated into 3D-space, // produces faces to skin a surface between consecutive profiles. Optionally, the first and last // profiles can have endcaps, or the last and first profiles can be skinned together. // The user is responsible for making sure the orientation of the first vertex of each profile are relatively aligned. // If called as a function, returns a VNF structure like `[VERTICES, FACES]`. See [VNF](vnf.scad). // If called as a module, creates a polyhedron of the skinned profiles. // The vertex matching algorithms are as follows: // - `"distance"`: Vertices between profiles are matched based on closest next position, relative to the center of each profile. // - `"angle"`: Vertices between profiles are matched based on closest next polar angle, relative to the center of each profile. // - `"evenly"`: Vertices are evenly matched between profiles, such that a point 30% of the way through one profile, will be matched to a vertex 30% of the way through the other profile, based on vertex count. // Arguments: // profiles = A list of 2D paths that have been moved and/or rotated into 3D-space. // closed = If true, the last profile is skinned to the first profile, to allow for making a closed loop. Assumes `caps=false`. Default: false // caps = If true, endcap faces are created. Assumes `closed=false`. Default: true // matching = Specifies the algorithm used to match up vertices between profiles, to create faces. Given as a string, one of `"distance"`, `"angle"`, or `"evenly"`. If given as a list of strings, equal in number to the number of profile transitions, lets you specify the algorithm used for each transition. Default: "distance" // Example(FlatSpin): // skin([ // move([0,0, 0], p=scale([2,1,1], p=path3d(circle(d=100,$fn=48)))), // move([0,0,100], p=path3d(circle(d=100,$fn=4))), // move([0,0,200], p=path3d(circle(d=100,$fn=12))), // ]); // Example(FlatSpin): // skin([ // for (ang = [0:10:90]) // rot([0,ang,0], cp=[200,0,0], p=path3d(circle(d=100,$fn=3+(ang/10)))) // ]); // Example(FlatSpin): Möbius Strip // skin([ // for (ang = [0:10:360]) // rot([0,ang,0], cp=[100,0,0], p=rot(ang/2, p=path3d(square([1,30],center=true)))) // ], caps=false); // Example(FlatSpin): Closed Loop // skin([ // for (i = [0:5]) // rot([0,i*60,0], cp=[100,0,0], p=path3d(circle(d=30,$fn=3+i%3))) // ], closed=true, caps=false); // Example: Distance Matching // skin([ // move([0,0, 0], p=scale([1,2,1],p=path3d(circle(d=50,$fn=36)))), // move([0,0,100], p=scale([2,1,1],p=path3d(circle(d=50,$fn=36)))) // ], matching="distance"); // Example: Angle Matching // skin([ // move([0,0, 0], p=scale([1,2,1],p=path3d(circle(d=50,$fn=36)))), // move([0,0,100], p=scale([2,1,1],p=path3d(circle(d=50,$fn=36)))) // ], matching="angle"); // Example: Evenly Matching // skin([ // move([0,0, 0], p=scale([1,2,1],p=path3d(circle(d=50,$fn=36)))), // move([0,0,100], p=scale([2,1,1],p=path3d(circle(d=50,$fn=36)))) // ], matching="evenly"); module skin(profiles, closed=false, caps=true, matching="distance") { vnf_polyhedron(skin(profiles, caps=!closed, closed=closed, matching=matching)); } function skin(profiles, closed=false, caps=true, matching="distance") = assert(is_list(profiles)) assert(is_bool(closed)) assert(is_bool(caps)) assert(!closed||!caps) assert(is_string(matching)||is_list(matching)) let( matching = is_list(matching)? matching : [for (pidx=idx(profiles,end=closed?-1:-2)) matching] ) assert(len(matching) == len(profiles)-closed?0:1) vnf_triangulate( concat([ for(pidx=idx(profiles,end=closed? -1 : -2)) let( prof1 = profiles[pidx%len(profiles)], prof2 = profiles[(pidx+1)%len(profiles)], cp1 = mean(prof1), cp2 = mean(prof2), midpt = (cp1+cp2)/2, n = normalize(cp2-cp1), n1 = plane_normal(plane_from_pointslist(prof1)), n2 = plane_normal(plane_from_pointslist(prof2)), perp = vector_angle(n1,n2)>0.01? vector_axis(n1,n2) : vector_angle(n,UP)>44? vector_axis(n,UP) : vector_axis(n,LEFT), perp1 = vector_axis(n1,perp), perp2 = vector_axis(n2,perp), poly1 = ccw_polygon(project_plane(prof1, cp1, cp1+perp, cp1+perp1)), poly2 = ccw_polygon(project_plane(prof2, cp2, cp2+perp, cp2+perp2)), match = matching[pidx], faces = [ for( first = true, finishing = false, finished = false, plen1 = len(poly1), plen2 = len(poly2), i=0, j=0, side=0; !finished; dang1 = abs(xy_to_polar(poly1[i%plen1]).y - xy_to_polar(poly2[(j+1)%plen2]).y), dang2 = abs(xy_to_polar(poly2[j%plen2]).y - xy_to_polar(poly1[(i+1)%plen1]).y), dist1 = norm(poly1[i%plen1] - poly2[(j+1)%plen2]), dist2 = norm(poly2[j%plen2] - poly1[(i+1)%plen1]), side = i>=plen1? 0 : j>=plen2? 1 : match=="angle"? (dang1>dang2? 1 : 0) : match=="distance"? (dist1>dist2? 1 : 0) : match=="evenly"? (i/plen1 > j/plen2? 0 : 1) : assert(in_list(matching[i],["angle","distance","evenly"]),str("Got `",matching,"'")), p1 = lift_plane(poly1[i%plen1], cp1, cp1+perp, cp1+perp1), p2 = lift_plane(poly2[j%plen2], cp2, cp2+perp, cp2+perp2), p3 = side? lift_plane(poly1[(i+1)%plen1], cp1, cp1+perp, cp1+perp1) : lift_plane(poly2[(j+1)%plen2], cp2, cp2+perp, cp2+perp2), face = [p1, p3, p2], i = i + (side? 1 : 0), j = j + (side? 0 : 1), first = false, finished = finishing, finishing = i>=plen1 && j>=plen2 ) if (!first) face ] ) vnf_add_faces(faces=faces) ], closed||!caps? [] : let( prof1 = profiles[0], prof2 = select(profiles,-1), ncl1 = sort(find_noncollinear_points(prof1)), ncl2 = sort(find_noncollinear_points(prof2)), pa1=prof1[ncl1.x], pa2=prof1[ncl1.y], pa3=prof1[ncl1.z], pb1=prof2[ncl2.x], pb2=prof2[ncl2.y], pb3=prof2[ncl2.z], poly1 = ccw_polygon(project_plane(prof1, pa1, pa2, pa3)), poly2 = clockwise_polygon(project_plane(prof2, pb1, pb2, pb3)) ) [ vnf_add_face(pts=lift_plane(poly1, pa1, pa2, pa3)), vnf_add_face(pts=lift_plane(poly2, pb1, pb2, pb3)) ]) ); // vim: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap