├── images ├── ParametricSVG.png ├── multiPathDemo.png ├── ExtendToBoundary.png ├── pathbuilder_welcome.png └── pathbuilder_right arrow.png ├── resources ├── Pathbuilder_arrows.png ├── repository-open-graph-template.pdn └── repository-open-graph-template.png ├── demo ├── quick_shape_demo.scad ├── points_first_demo.scad ├── mesh_tween_demo.scad ├── meshbuilder_Spiral.scad ├── boundary_demo.scad ├── test_shapes_demo.scad ├── TaperedThread.scad ├── TwistedMesh.scad ├── project_case.scad ├── step_by_step_debug_demo.scad ├── parametric_modules_demo.scad ├── tween_demo.scad ├── readme.md ├── using_modules_demo.scad └── multi_path_demo.scad ├── LICENSE ├── meshbuilder.scad ├── shapes └── vslot4040.scad ├── offset.scad ├── README.md ├── pointutils.scad └── pathbuilder.scad /images/ParametricSVG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/images/ParametricSVG.png -------------------------------------------------------------------------------- /images/multiPathDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/images/multiPathDemo.png -------------------------------------------------------------------------------- /images/ExtendToBoundary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/images/ExtendToBoundary.png -------------------------------------------------------------------------------- /images/pathbuilder_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/images/pathbuilder_welcome.png -------------------------------------------------------------------------------- /images/pathbuilder_right arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/images/pathbuilder_right arrow.png -------------------------------------------------------------------------------- /resources/Pathbuilder_arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/resources/Pathbuilder_arrows.png -------------------------------------------------------------------------------- /resources/repository-open-graph-template.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/resources/repository-open-graph-template.pdn -------------------------------------------------------------------------------- /resources/repository-open-graph-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinther/pathbuilder/HEAD/resources/repository-open-graph-template.png -------------------------------------------------------------------------------- /demo/quick_shape_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | // The official Nike logo swoosh. 4 | svgShape("M68.56-4L18.4-25.36Q12.16-28 7.92-28q-4.8 0-6.96 3.36-1.36 2.16-.8 5.48t2.96 7.08q2 3.04 6.56 8-1.6-2.56-2.24-5.28-1.2-5.12 2.16-7.52Q11.2-18 14-18q2.24 0 5.04 .72z"); 5 | 6 | -------------------------------------------------------------------------------- /demo/points_first_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | pb_petal = "m0 -1c-25-30 25-30 0,0 z"; 4 | $pb_spline=66; // We want 36 line segments on each spline. 5 | pts = svgPoints(pb_petal)[0]; // Generate points list for the svg path. 6 | // Do your thing to the points here 7 | for(a=[0:120:360]) rotate([0,15,a]) polygon(pts); 8 | -------------------------------------------------------------------------------- /demo/mesh_tween_demo.scad: -------------------------------------------------------------------------------- 1 | use 2 | use 3 | 4 | $fn=32; 5 | 6 | c1 = "M2,0 fillet3,9 L6,9 fillet16,6 L12,15 H-12 L-6,9 fillet16,6 L-2,0 fillet3,9"; 7 | c2 = "M10,0 fillet8,9 L14,10 fillet16,6 L15,15 H-15 L-14,10 fillet16,6 L-10,0 fillet8,9"; 8 | 9 | pl = [for(i=[0:30]) svgPoints(svgTweenPath(c1, c2, i/30), i)[0]]; 10 | buildMeshFromPointLayers(pl, true, true, true); 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/meshbuilder_Spiral.scad: -------------------------------------------------------------------------------- 1 | use 2 | use 3 | use 4 | $fn=32; 5 | 6 | module spiral(shape, angle, step, pitch){ 7 | l = len(shape); 8 | pl = [for(j=[l:-1:0]) let(k=(j==l? 0:j)) [for(i=[0:step:angle-1]) 9 | rotatePoint( [shape[k][0],shape[k][1], shape[k][2]+i*pitch/360], i)]]; 10 | n = len(pl[0]); 11 | // Manually add end caps 12 | front_cap = [for(i=[0:l-1]) n*i]; 13 | end_cap = [for(i=[l:-1:1]) n*i-1]; 14 | faces = [front_cap, end_cap]; 15 | buildMeshFromPointLayers(pl, true, false, false, false, faces); 16 | } 17 | 18 | points = svgPoints("m5,0 h1 a10,10,0,0,1,10,0h1,v-2 a12,12,0,0,0,-12,0")[0]; 19 | //echo(points); 20 | //points = [[M]]; 21 | // swap y and z axis 22 | shape = [for(pt = points) [pt[0],0,pt[1]]]; 23 | 24 | spiral(shape, 720, 5, 14); 25 | -------------------------------------------------------------------------------- /demo/boundary_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | // helper function to visualize a polyline 4 | module draw_polyline(pts, w = 0.25, c=false) { 5 | pts=c? concat(pts,[pts[0]]) : pts; 6 | for(i = [0:len(pts)-2]) hull(){ translate(pts[i]) circle(d=w,$fn=16); translate(pts[i+1]) circle(d=w); } 7 | } 8 | 9 | // Boundary 10 | // Demonstrates how a path can be constructed as a string and how the 11 | // Forward command can be used to ray cast a line at a given angle to a list of points representing boundary line segments 12 | boundary = "0 0 0 10 5 10 20 20 30 10"; 13 | linear_extrude(0.1) draw_polyline(svgPoints(str("M0 0L",boundary))[0]); // Show where the boundary is 14 | 15 | for (i=[-80:10:80]){ 16 | pts = svgPoints(str("m2 0h7angle",(i),"Forward",boundary))[0]; 17 | //Do your thing to the points here 18 | if (len(pts)>2) color(rands(0,1,3),0.3) linear_extrude(0.081+i/1000) polygon(pts); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /demo/test_shapes_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | //example shapes 4 | pb_share ="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3 -3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"; 5 | pb_arrow = "m 0 0chamfer10h20fillet2v10segment10 10 10h10fillet2v-10fillet2l35 20fillet2L40 50fillet2v-10h-10Segment0 10-30z"; 6 | pb_polar = "m0 0Polar 20 30segment20 0 -15 64forward20V0"; 7 | pb_swoosh ="M68.56-4L18.4-25.36Q12.16-28 7.92-28q-4.8 0-6.96 3.36-1.36 2.16-.8 5.48t2.96 7.08q2 3.04 6.56 8-1.6-2.56-2.24-5.28-1.2-5.12 2.16-7.52Q11.2-18 14-18q2.24 0 5.04 .72z"; 8 | pb_petal = "m0 -1c-25-30 25-30 0,0 z"; 9 | pb_drop = "m60 90 -3 -10a3.1 3.1 0 1 1 6 0"; 10 | pb_square = "M0 0 h1 v1 h-1 Z"; 11 | 12 | svgShape(pb_share); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Paul van Dinther 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/TaperedThread.scad: -------------------------------------------------------------------------------- 1 | use 2 | use 3 | use 4 | 5 | $fn=8; 6 | 7 | module taperedScrew(startRadius = 5, endRadius = 2, angle = 0, pitch=2, threadDepthStart=1.5, threadDepthEnd=0.1, threadShapeRatio=0.5){ 8 | segmentsPerCircle = pb_segmentsPerCircle(startRadius); 9 | zStep = pitch / segmentsPerCircle; 10 | steps = angle / 360 * segmentsPerCircle; 11 | angleStep = angle / steps; 12 | radiusStep = (endRadius - startRadius) / steps; 13 | threadDepthStep = (threadDepthEnd - threadDepthStart) / steps; 14 | pl = [for (i=[0:steps]) let(h=threadDepthStart+(i*threadDepthStep)) 15 | rotatePoints(pts=translatePoints([[0, 0, 0], [radiusStep*segmentsPerCircle, 0, h*threadShapeRatio], [h, 0, h*threadShapeRatio*0.5], [0, 0, 0]],[startRadius+(i*radiusStep),0,0]), angles=[0,0,i * angleStep], z_offset = i * zStep) 16 | ]; 17 | echo(pl[1]); 18 | union(){ 19 | buildMeshFromPointLayers(pl, true, true,true, true, true); 20 | cylinder(h = steps * zStep, r1=startRadius, r2=endRadius); 21 | } 22 | 23 | } 24 | 25 | taperedScrew(startRadius=5, endRadius = 0.5, angle=360 * 10, pitch=4, threadDepthStart=1.0, threadDepthEnd=0.0, threadShapeRatio=1, $fn=8); 26 | -------------------------------------------------------------------------------- /demo/TwistedMesh.scad: -------------------------------------------------------------------------------- 1 | use 2 | use 3 | use 4 | 5 | $fn = 32; 6 | 7 | angle=180; 8 | radius = 30; 9 | 10 | // Building a 2D profile 11 | // We define a start shape and end shape and use the svgTweenPath. 12 | // to generate the path definitions in between. This way we can 13 | // keep a consistent radius in the model. 14 | 15 | start_shape = "m0,0v5h27.5fillet3V40fillet8H60v-5h-27.5fillet3V0fillet8H0"; 16 | end_shape = "m0,0v5h27.5fillet3V20fillet8H60v-5h-27.5fillet3V0fillet8H0"; 17 | 18 | // This is what the shapes looks like 19 | 20 | translate([30,40,0]) color("blue") polygon(svgPoints(svgTweenPath(start_shape, end_shape, 0))[0]); 21 | translate([-30,40,0]) color("red") mirror([-1,0,0]) polygon(svgPoints(svgTweenPath(start_shape, end_shape, 1))[0]); 22 | 23 | // Building the mesh 24 | 25 | // Now we are going to manipulate the shape point list 26 | // many times inside a loop counting from 0 to 1 in small steps 27 | // You see here several manipulation functions nested. 28 | 29 | lp = [for (i=[0:0.002:1]) rotatePoints(translatePoints(svgPoints(svgTweenPath(start_shape, end_shape,i))[0],[radius, 0, 0]),[0,i*-angle,0])]; 30 | buildMeshFromPointLayers(lp, true, true, true,true); 31 | 32 | // Multiply the number of paths and points per path 33 | // to get the total number of vertices for the mesh. 34 | echo(str(len(lp) * len(lp[0]), " vertices used")); 35 | -------------------------------------------------------------------------------- /demo/project_case.scad: -------------------------------------------------------------------------------- 1 | use 2 | 3 | // project case 4 | 5 | width = 40; 6 | length = 60; 7 | height = 30; 8 | thickness = 2; 9 | radius = 1.2; 10 | 11 | module outer(width, length, radius){ 12 | $fn = 64; 13 | m(0,0) 14 | fillet(radius) 15 | v(width) 16 | fillet(radius) 17 | h(length) 18 | fillet(radius) 19 | v(-width) 20 | fillet(radius); 21 | } 22 | 23 | module inner(width, length, radius){ 24 | $fn = 64; 25 | m(0,0) 26 | fillet(-radius) 27 | v(width) 28 | fillet(-radius) 29 | h(length) 30 | fillet(-radius) 31 | v(-width) 32 | fillet(-radius); 33 | } 34 | 35 | module case(width, length, height, thickness, radius){ 36 | $fn = 64; 37 | union(){ 38 | linear_extrude(thickness){ 39 | outer(width, length, thickness); 40 | } 41 | translate([0,0,thickness]) linear_extrude(height - thickness){ 42 | difference(){ 43 | outer(width, length, thickness); 44 | translate([thickness, thickness]) inner(width - (thickness * 2), length- (thickness * 2), thickness); 45 | translate([thickness, thickness]) circle(r = radius); 46 | translate([thickness, width - thickness]) circle(r = radius); 47 | translate([length - thickness, thickness]) circle(r = radius); 48 | translate([length - thickness, width - thickness]) circle(r = radius); 49 | } 50 | } 51 | } 52 | } 53 | 54 | case(width, length, height, thickness, radius); 55 | -------------------------------------------------------------------------------- /demo/step_by_step_debug_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | // laser cut bracket before folding 4 | pb_yield ="m0 0fillet3v30fillet3h17a2.3 2.3 0 1 1 3 3v17fillet9h60fillet9v-17a2.3 2.3 0 1 1 3-3h60chamfer10V0chamfer10z"; 5 | 6 | $fa=6; // new segment when angle reaches 6 degrees 7 | $fs=0.4; // don't make a new segment when it is smaller than 0.4 units 8 | 9 | // Have a look and see what is going on inside. 10 | // normally you would just call: svgShape(pb_yield); 11 | // but here we pull the steps apart to show what goes on. 12 | 13 | // parse the command string and make sense of them. 14 | cmds = pb_tokenizeSvgPath(pb_yield); 15 | 16 | echo("************tokenized commands************"); 17 | for (c=cmds) echo(c); 18 | 19 | // process the commands into a list of shape data. Each shape consists of a point list and a list of post processing commands. 20 | shape_list = pb_processCommands(cmds); 21 | shape1 = shape_list[0]; 22 | 23 | echo("************post processing commands************"); 24 | for (p=shape1[1]){ 25 | echo(p, 26 | p[0]==0? str("start index:", p[1]) : 27 | p[0]==2? str("fillet index:", p[1], " radius:", p[2]) : 28 | p[0]==3? str("chamfer index:", p[1], " size:", p[2]) : 29 | p[0]==4? str("end index:", p[1]) : 30 | str("error ",p[0]," unknown") 31 | ); 32 | } 33 | // This yellow shape is what we know before post processing. 34 | // Fillets and chamfers are not yet applied. 35 | translate([0,0,-1]) color("yellow") linear_extrude(1) polygon(shape1[0]); 36 | 37 | // apply the post processing steps 38 | pts = pb_postProcessPathLists(shape_list)[0]; 39 | 40 | // The final result 41 | color("red") linear_extrude(1) polygon(pts); 42 | -------------------------------------------------------------------------------- /demo/parametric_modules_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | col = [[0.109, 0.277, 0.715],[0.996, 0.449, 0.707],[0.832, 0.113, 0.102],[0.953, 0.363, 0.137],[0.980, 0.789, 0.066]]; 4 | // Code with params 5 | // Here parameters and the loop variable are used directly in pathbuilder commands resulting in multiple arrows branching out. 6 | 7 | aw = 20; // arrow shaft width 8 | $fa=6; // new segment when angle reaches 6 degrees 9 | $fs=0.4; // don't make a new segment when it is smaller than 0.4 units 10 | 11 | for (al=[10:20:90]) color(col[(al-10)/20]) 12 | translate([20, 20, 0]) 13 | linear_extrude(10-al/10) 14 | m(0,0) // start at origin 15 | chamfer(10) // apply a chamfer of 10 long at this point 16 | h(aw) // move horizontal along by given distance 17 | fillet(2) // apply a fillet with given radius 18 | v(al*2) // move vertical along by given distance 19 | fillet(al) // apply a fillet with given radius 20 | h(al*2) // move horizontal along by given distance 21 | fillet(2) // apply a fillet with given radius 22 | v(-aw/2) // move vertical along by given distance 23 | fillet(2) // apply a fillet with given radius 24 | l(35,aw) // move along by given x and y distance 25 | fillet(2) // apply a fillet with given radius 26 | l(-35, aw) // move along by given x and y distance 27 | fillet(2) // apply a fillet with given radius 28 | v(-aw/2) // move vertical along by given distance 29 | H(0) // move horizontal along to given x coordinate 30 | fillet(al+20); // apply a fillet with given radius -------------------------------------------------------------------------------- /demo/tween_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | // This demo takes two svg paths and morphs the path parameters to create a 3D mesh 4 | 5 | $pb_spline = 20; 6 | 7 | function scaleXYPoints(pts, pos_scale=[1,1,1], neg_scale=[1,1,1]) = [for(pt=pts) [pt[0]<0? pt[0]*neg_scale[0]: pt[0]*pos_scale[0],pt[1]<0? pt[1]*neg_scale[1]: pt[1]*pos_scale[1],pt[2]<0? pt[2]*neg_scale[2]: pt[2] * pos_scale[2]]]; 8 | 9 | module buildMeshFromPointLayers(pointLayers = [], sides = true, bottom=true, top=true){ 10 | n = len(pointLayers[0]); 11 | pts = [for (deck=[0:1:len(pointLayers)-1]) each pointLayers[deck]]; 12 | faces = [for (d = [0:1:len(pointLayers)-2], p = [0:1:len(pointLayers[d])-2]) let(c = (n * d)+ p) [c,c+1, c+n+1,c+n]]; 13 | bottom_points = bottom? pointLayers[len(pointLayers)-1] : []; 14 | bottom_faces = bottom? [for(i=[len(bottom_points)-1:-1:0]) i] : []; 15 | top_points = top? pointLayers[0] : []; 16 | top_faces = top? [for(i=[len(pts) - len(top_points):len(pts)-1]) i] : []; 17 | with_top_faces = top? concat(sides? faces: [], [top_faces]) : sides? faces: []; 18 | all_faces = bottom? concat(with_top_faces, [bottom_faces]) : with_top_faces; 19 | polyhedron(points = pts, faces = all_faces, convexity = 10); 20 | } 21 | 22 | // Note that the path must be closed with Z. This is not strictly nessesary when used with polygons because 23 | // polygons are always closed 24 | 25 | p1 = "M -1170 0 V -185 C -980 -336 -738 -305 -507 -305 H 300 C 668 -305 882 -124 1034 -66 C 1179 -8 1260 -13 1260 0Z"; 26 | p2 = "M -1200 0 V -250 C -922 -353 -738 -345 -507 -345 H 400 C 548 -345 815 -287 1034 -215 C 1241 -143 1400 -57 1400 0Z"; 27 | 28 | point_layers = [for (i=[0:0.05:1]) scaleXYPoints(svgPoints(svgTweenPath(p1, p2, i),(1-cos(i*90))*150)[0],[1-sin(i*140)*0.1,1,1],[1,1,1])]; 29 | buildMeshFromPointLayers(point_layers, sides=true, bottom=true, top=true); 30 | mirror([0,1,0]) buildMeshFromPointLayers(point_layers, sides=true, bottom=true, top=true); 31 | -------------------------------------------------------------------------------- /demo/readme.md: -------------------------------------------------------------------------------- 1 | ### TwistedMesh.scad 2 | PathBuilder, MeshBuilder and pointutils in action together. With these tools you have absolute control over the dimensions and it is FAST. No boolean operations. Meshbuilder just spits out a mesh. 3 | ![image](https://github.com/dinther/pathbuilder/assets/1192916/588b3a11-b7cd-40ed-bc5d-cd8ffc9ce3be) 4 | 5 | ``` 6 | use 7 | use 8 | use 9 | 10 | $fn = 32; 11 | 12 | angle=180; 13 | radius = 30; 14 | 15 | // Building a 2D profile 16 | // We define a start shape and end shape and use the svgTweenPath. 17 | // to generate the path definitions in between. This way we can 18 | // keep a consistent radius in the model. 19 | 20 | start_shape = "m0,0v5h27.5fillet3V40fillet8H60v-5h-27.5fillet3V0fillet8H0"; 21 | end_shape = "m0,0v5h27.5fillet3V20fillet8H60v-5h-27.5fillet3V0fillet8H0"; 22 | 23 | // This is what the shapes looks like 24 | 25 | color("blue") polygon(svgPoints(svgTweenPath(start_shape, end_shape, 0))[0]); 26 | color("red") polygon(svgPoints(svgTweenPath(start_shape, end_shape, 1))[0]); 27 | 28 | // Building the mesh 29 | 30 | // Now we are going to manipulate the shape point list 31 | // many times inside a loop counting from 0 to 1 in small steps 32 | // You see here several manipulation functions nested. 33 | 34 | lp = [for (i=[0:0.002:1]) rotatePoints(translatePoints(svgPoints(svgTweenPath(start_shape, end_shape,i))[0],[radius, 0, 0]),[0,i*-angle,0])]; 35 | buildMeshFromPointLayers(lp, true, true, true,true); 36 | 37 | // Multiply the number of paths and points per path 38 | // to get the total number of vertices for the mesh. 39 | echo(str(len(lp) * len(lp[0]), " vertices used")); 40 | ``` 41 | ### project_case.scad 42 | ![image](https://user-images.githubusercontent.com/1192916/153139548-ab34fd3d-5e7c-433b-9cf6-48fa8a1eebe7.png) 43 | 44 | ### Meshbuilder spiral 45 | An advanced demo of a spiral generated with meshbuilder. 46 | ![image](https://github.com/dinther/pathbuilder/assets/1192916/bcef11be-5208-4ec0-b845-a0dca418b3fa) 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/using_modules_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | pb_arrow = "m 0 0chamfer4h5fillet1v20fillet10h20fillet1v-5fillet1l20 7.5fillet1l-20 7.5fillet1v-5H0fillet15z"; 4 | 5 | // In code 6 | // Pathbuilder can also be used directly in code. The same commands apply but here we are using the openSCAD modules 7 | // The advantage here is that you can directly use your parameters as further shown in the last demo. 8 | // Also a demonstration of the use of $fa and $fs. Pathbuilder respects $fn, $fa and $fs in it's approximation of circle and ellipse curves. 9 | 10 | $fa=6; // new segment when angle reaches 6 degrees 11 | $fs=0.4; // don't make a new segment when it is smaller than 0.4 units 12 | 13 | m(0,0) // start at origin 14 | chamfer(4) // apply a chamfer of 4 long at this point 15 | h(5) // move horizontal along by 5 16 | fillet(1) // apply a fillet with a radius of 1 17 | v(20) // move vertical up by 20 18 | fillet(10) // apply a fillet with a radius of 10 19 | h(20) // move horizontal along by 20 20 | fillet(1) // apply a fillet with a radius of 1 21 | v(-5) // move vertical down by 5 22 | fillet(1) // apply a fillet with a radius of 1 23 | l(20,7.5) // move right by 20 and up by 7.5 24 | fillet(1) // apply a fillet with a radius of 1 25 | l(-20,7.5) // move left by 20 and up by 7.5 26 | fillet(1) // apply a fillet with a radius of 1 27 | v(-5) // move vertical down by 5 28 | fillet(1) // apply a fillet with a radius of 1 29 | H(0) // move horizontal to x coordinate 0 30 | fillet(15); // apply a fillet with a radius of 15 31 | 32 | // this red arrow uses the same commands in string format. 33 | color("red") translate([15,-15]) 34 | svgShape("m 0 0chamfer4h5fillet1v20fillet10h20fillet1v-5fillet1l20 7.5fillet1l-20 7.5fillet1v-5H0fillet15z"); 35 | 36 | -------------------------------------------------------------------------------- /demo/multi_path_demo.scad: -------------------------------------------------------------------------------- 1 | include 2 | 3 | // This path command string will draw a magnifying glass. 4 | // It is made of two paths. 5 | // The first path outlines the entire magnifying glass Clockwize (CW) 6 | // The second path outlines the inner hole Counter Clockwize (CCW) 7 | 8 | // It does not matter if you start the first path Clockwize or Counter clockwize 9 | // But ever path after the first will be subtracted when it has an opposite winding 10 | // and added if it has the same winding. 11 | 12 | svgShape("m20 0 40 40 -5 5 5 5a50 50 0 1 1 -10 10l-5-5-5 5-40-40m60 50a35 35 0 1 0 10 -10"); 13 | 14 | 15 | 16 | // This path draws a square with patches cut out of it. 17 | // It is made of 5 paths. 18 | // The first path is a 200 x 200 square and the rest 5 x 5 squares. 19 | // Path 1 is Counter Clockwize (CCW) 20 | // Path 2 on the left edge, is Clockwize (CW) different from Path 1 and thus subtracted. 21 | // Path 3 on the right edge, is Clockwize (CW) different from Path 1 and thus subtracted. 22 | // Path 4 in top left quadrant, is is Clockwize (CW) different from Path 1 and thus subtracted. 23 | // Path 5 in the center is Counter Clockwize (CCW) the same as Path 1 and thus added. 24 | 25 | // However, subtraction and addition in SVG works different from the normal openSCAD 26 | // difference() and union commands. Implementation is complex and goes beyond the scope of Pathbuilder. 27 | // 28 | // It does not matter if you start Clockwize or Counter clockwize with the firstpath. 29 | // However, subsequent paths are added when the winding matches the winding of the first path, 30 | // and subtracted when the winding does not patch the winding of the first path. 31 | // The order in which the paths appear after the first one matters when their windings differ. 32 | // Addition or subtration of a path is applied to all the paths before it. 33 | 34 | // You can also create multiple polygon shapes and apply the usual openSCAD operations. 35 | 36 | translate([-250,0,0]) 37 | svgShape("m0 0h200v200h-200zM-25 75v50h50v-50zM175 75v50h50v-50zM100 100v50h50v-50zM75 75h50v50h-50"); 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /meshbuilder.scad: -------------------------------------------------------------------------------- 1 | // meshbuilder 2 | // 3 | // This libray is designed to be used in conjuction with pathbuilder to create complex 3D shapes. 4 | // With pathbuilder (https://github.com/dinther/pathbuilder) it is easy to morph from one complex 2D shape 5 | // to another similar 2D shape. (See svgTweenPath command in pathbuilder for the limitations) 6 | // 7 | // A mesh can be stiched by stacking multiple 2D shapes on top of each other. 8 | // kinda the reverse of how a slicer turns a 3D object in many outlines. 9 | // 10 | // The number of points on each stacked 2D shape need to match. 11 | // 12 | // pointLayers is a list containing path lists with 3D points. 13 | // The module demo shows a simple rectangle that changes shape as it is moved up. 14 | // This shape can not be achieved with the twist parameter in linear_extrude. 15 | // 16 | // Latest version here: https://github.com/dinther/pathbuilder 17 | // 18 | // By: Paul van Dinther 19 | 20 | 21 | // TODO: aligning the start of the paths 22 | // TODO: learn about smarts to make better stiching decisions 23 | // 24 | 25 | // buildMeshFromPointLayers(pointLayers, sides, bottom, top, closed, faces) 26 | // 27 | // Generates a mesh based on a list of a list of points for each layer 28 | // pointLayers (list) List of 3D points 29 | // sides (bool) Generate the sides of the mesh 30 | // bottom (bool) Generate the bottom of the mesh 31 | // top (bool) Generate the top of the mesh 32 | // closed (bool) Set to true if the side of the mesh should be closed 33 | // faces (list) User provided list of faces 34 | // return (number) Angle between the two vectors in degrees. 35 | module buildMeshFromPointLayers(pointLayers = [], sides = true, bottom=true, top=true, closed=true, faces=[]){ 36 | n = len(pointLayers[0]); 37 | pts = [for (deck=[0:1:len(pointLayers)-1]) each pointLayers[deck]]; 38 | side_faces = [for (d = [0:1:len(pointLayers)-2], p = [0:1:len(pointLayers[d])-(closed? 1 : 2)]) let(c = (n * d)+ p, last=p+1==len(pointLayers[d]) ) [c,last? d*n: c+1, last? c+1 : c+n+1, c+n]]; 39 | bottom_faces = bottom? [[for(i=[n-1:-1:0]) i]] : []; 40 | top_faces = top? [[for(i=[len(pts) - n:len(pts)-1]) i]] : []; 41 | polyhedron(points = pts, faces = concat(side_faces, bottom_faces, top_faces, faces), convexity = 10); 42 | } 43 | 44 | function rotateAroundZ(x, y, z, angle) = let ( 45 | c = cos(angle), 46 | s = sin(angle), 47 | nx = (c * x) + (s * y), 48 | ny = (c * y) - (s * x)) [nx, ny, z]; 49 | 50 | 51 | module demo(){ 52 | steps = 40; 53 | l1 = 20; 54 | l2 = 5; 55 | ls = (l2-l1)/steps; 56 | w1 = 8; 57 | w2 = 0; 58 | ws = (w2-w1)/steps; 59 | h = 50; 60 | hs = h / steps; 61 | wh = w1 * 0.5; 62 | 63 | a = 90; 64 | as = a/steps; 65 | points = [[0,wh],[l1, wh],[l1,-wh],[0,-wh]]; 66 | pl = [for (i=[0:steps]) 67 | [rotateAroundZ(points[0][0], points[0][1], i*hs, i*as), 68 | rotateAroundZ(points[1][0]+i*ls, points[1][1]+ i*ws*0.5, i*hs, i*as), 69 | rotateAroundZ(points[2][0]+i*ls, points[2][1]- i*ws*0.5, i*hs, i*as), 70 | rotateAroundZ(points[3][0], points[3][1], i*hs, i*as)] 71 | ]; 72 | 73 | buildMeshFromPointLayers(pl, true, true,true); 74 | } 75 | 76 | demo(); 77 | -------------------------------------------------------------------------------- /shapes/vslot4040.scad: -------------------------------------------------------------------------------- 1 | use 2 | use 3 | 4 | // Examples 5 | // 6 | // 400mm vslot 4040 profile 7 | // vslot4040(400); 8 | // 9 | // Cap 10 | // vslot4040Cap(4, centerHoleDiameter=10, insertLength=4, bolt_recess=2, $fn=32); 11 | // 12 | // Internal slider 13 | 14 | module vslot4040_outer(offset = 0, filletRadius = 1){ 15 | fr = str(filletRadius); 16 | p = str("M-14.66955,20 l1.7,-1.7 v-0.61h -2.4 v-1.6l 2.37,-2.37 h5.99921 l2.37,2.37 v1.6 h-2.4 v0.61 l1.7,1.7 h10.6609 l1.7,-1.7 v-0.61 h-2.4 v-1.6 l2.37,-2.37 h5.99921 l2.37,2.37 v1.6 h-2.4 v0.61 l1.7,1.7 h5.33045 fillet ", fr, " v-5.33045 l-1.7,-1.7 h-0.61 v2.4 h-1.6 l-2.37,-2.37 v-5.99921 l2.37,-2.37 h1.6 v2.4 h0.61 l1.7,-1.7 v-10.6609 l-1.7,-1.7 h -0.61 v2.4 h-1.6 l-2.37,-2.37 v-5.99921 l2.37,-2.37 h1.6 v2.4 h0.61 l1.7,-1.7 v-5.33045 fillet ", fr, " h-5.33045 l-1.7,1.7 v0.61 h2.4 v1.6 l-2.37,2.37 h-5.99921 l-2.37,-2.37 v-1.6 h2.4 v-0.61 l-1.7,-1.7 h-10.6609 l-1.7,1.7 v0.61 h2.4 v1.6 l-2.37,2.37 h-5.99921 l-2.37,-2.37 v-1.6 h2.4 v-0.61 l-1.7,-1.7 h-5.33045 fillet ", fr, " v5.33045 l1.7,1.7 h0.61 v-2.4 h1.6 l2.37,2.37 v5.99921 l-2.37,2.37 h-1.6 v-2.4 h-0.61 l-1.7,1.7 v10.6609 l1.7,1.7 h0.61 v-2.4 h1.6 l 2.37,2.37 v5.99921 l-2.37,2.37 h-1.6 v-2.4 h-0.61 l-1.7,1.7 v5.33045 fillet ", fr); 17 | pts = svgPoints(p)[0]; 18 | pts1 = (len(pts) > 1 && pts[0][0] == pts[len(pts)-1][0] && pts[0][1] == pts[len(pts)-1][1])? [for(i=[0:len(pts)-2]) pts[i]] : pts; 19 | pts2 = offset== 0? pts1 : offset_pts(pts1, -offset); 20 | polygon(pts2); 21 | } 22 | 23 | module vslot4040_inner(offset=0){ 24 | p = "M-12.91,6.1 h6.91 chamfer1.2 v6.91 l3.58,3.58 h5.04 l3.58,-3.58 v-6.91 chamfer1.2 h6.91 l3.58,-3.58 v-5.04 l-3.58,-3.58 h-6.91 chamfer1.2 v-6.91 l-3.58,-3.58 h-5.04 l-3.58,3.58 v6.91 chamfer1.2 h-6.91 l-3.58,3.58 v5.04"; 25 | pts = svgPoints(p)[0]; 26 | pts1 = (len(pts) > 1 && pts[0][0] == pts[len(pts)-1][0] && pts[0][1] == pts[len(pts)-1][1])? [for(i=[0:len(pts)-2]) pts[i]] : pts; 27 | pts2 = offset== 0? pts1 : offset_pts(pts1, -offset); 28 | polygon(pts2); 29 | } 30 | 31 | module vslot4040(length, filletRadius = 1){ 32 | linear_extrude(length) 33 | difference(){ 34 | vslot4040_outer(filletRadius); 35 | vslot4040_inner(0); 36 | translate([10,10]) circle(d=4.6); 37 | translate([10,-10]) circle(d=4.6); 38 | translate([-10,10]) circle(d=4.6); 39 | translate([-10,-10]) circle(d=4.6); 40 | } 41 | } 42 | 43 | module vslot4040Cap(height, centerHoleDiameter = 0, insertLength=0, innerOffset=-0.2, bolt_recess=0, width = 40, length=40, filletRadius = 1){ 44 | difference(){ 45 | union(){ 46 | linear_extrude(height) 47 | M(-width * 0.5, -length * 0.5) 48 | fillet(filletRadius) 49 | h(width) 50 | fillet(filletRadius) 51 | v(length) 52 | fillet(filletRadius) 53 | h(-width) 54 | fillet(filletRadius); 55 | if (insertLength > 0){ 56 | translate([0,0,height]) linear_extrude(insertLength) vslot4040_inner(offset = innerOffset); 57 | } 58 | } 59 | 60 | // bolt holes 61 | translate([-10,-10,-1]) cylinder(d=5.1, h=height+2); 62 | translate([10,-10,-1]) cylinder(d=5.1, h=height+2); 63 | translate([-10,10,-1]) cylinder(d=5.1, h=height+2); 64 | translate([10,10,-1]) cylinder(d=5.1, h=height+2); 65 | 66 | if (bolt_recess>0){ 67 | translate([-10,-10,-1]) cylinder(d=9, h=bolt_recess+1); 68 | translate([10,-10,-1]) cylinder(d=9, h=bolt_recess+1); 69 | translate([-10,10,-1]) cylinder(d=9, h=bolt_recess+1); 70 | translate([10,10,-1]) cylinder(d=9, h=bolt_recess+1); 71 | } 72 | 73 | 74 | if (centerHoleDiameter> 0){ 75 | translate([0,0,-1]) cylinder(d=centerHoleDiameter, h=height + insertLength+2); 76 | } 77 | 78 | } 79 | } 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /offset.scad: -------------------------------------------------------------------------------- 1 | // openSCAD polyline offset routine retrieved from newgroup 2 | // https://forum.openscad.org/Polygon-Offset-Function-td17186.html 3 | 4 | // given a polygon, offset each vertex using the corner offset method above 5 | // note: this can produce self-intersections depending on the 'curvature' and offset 6 | function offset_pts(pts, dist) = 7 | _offset_poly(_iterative_remove_edges(pts, dist), dist); 8 | 9 | 10 | module test(){ 11 | pts1 = [[35, -8], [35, 7], [20, 12], [-10, 60], [-25, 65], [-40, 60], [-30, 50], [-21, 18], [-21, -18], [-30, -50], [-40, -60], [-25, -65], [-10, -60], [20, -13]]; 12 | pts2 = offset_pts(pts1, -0.5); 13 | difference(){ 14 | translate([0,0,1]) color("red") polygon(pts1); 15 | polygon(pts2); 16 | } 17 | polygon(pts2); 18 | } 19 | 20 | test(); 21 | 22 | 23 | 24 | // Helper functions 25 | 26 | // given two points, a, b, find equation for line that is parallel to line 27 | // segment but offset to the right by offset dist 28 | // equation is of the form c*x+d*y=e 29 | // represented as array [ c, d, e ] 30 | function _seg2eq(pa, pb, dist) = 31 | let (ab = [pb[0]-pa[0], pb[1]-pa[1]]) 32 | let (abl_un = [-ab[1], ab[0]]) 33 | let (abl_len = sqrt(abl_un[0]*abl_un[0] + abl_un[1]*abl_un[1])) 34 | let (abl = [ abl_un[0]/abl_len, abl_un[1]/abl_len ]) 35 | [ abl[0], abl[1], abl[0]*pa[0] + abl[1]*pa[1] - dist ]; 36 | 37 | // given two equations for lines, solve two equations to find intersection 38 | function _solve2eq(eq1, eq2) = 39 | let (a=eq1[0], b=eq1[1], c = eq1[2], d=eq2[0], e=eq2[1], f=eq2[2]) 40 | let (det=a*e-b*d) 41 | [ (e*c-b*f)/det, (-d*c+a*f)/det ]; 42 | 43 | // given a corner as two line segments, AB and BC, find the new corner B' that results 44 | // when both line segments are offet. Works by generating two equations and then solving 45 | function _offset_corner(pa, pb, pc, dist) = 46 | _solve2eq(_seg2eq(pa, pb, dist), _seg2eq(pb, pc, dist)); 47 | 48 | //echo(_ensurePathNotClosed([[1,2],[2,3],[3,4],[1,2]])); 49 | 50 | // given a polygon, offset each vertex using the corner offset method above 51 | // note: this can produce self-intersections depending on the 'curvature' and offset 52 | function _offset_poly(p, dist) = 53 | [ 54 | for (i=[0:len(p)-1]) 55 | i == 0 ? _offset_corner(p[len(p)-1], p[i], p[i+1], dist) : 56 | i == len(p)-1 ? _offset_corner(p[i-1], p[i], p[0], dist) : 57 | _offset_corner(p[i-1], p[i], p[i+1], dist) 58 | ]; 59 | 60 | // each segment of a polygon will 'vanish' at some offset value (unless adjacent 61 | // segments are parallel). Compute the offset at which each segment vanishes 62 | // (produces 'nan' or plus or minus infinity when adjacent segments parallel) 63 | function _offset_limit(p) = 64 | let (N = len(p)) 65 | let (offp = _offset_poly(p, 1)) // each vertex adjusted for unit of offset 66 | [ for (i=[0:N-1]) // for each segment 67 | // equations for segment for each vertex (angle bisector) 68 | let (eqv1 = _seg2eq(p[i], offp[i], 0), eqv2 = _seg2eq(p[(i+1)%N], offp[(i+1)%N], 0)) 69 | let (singv = _solve2eq(eqv1, eqv2)) // 'singular' vertex where edge vanishes 70 | let (offv = offp[i]-p[i]) // vertex shifts this much per unit of offset 71 | let (targetv = singv - p[i]) // what offset produces this coordinate? 72 | // essentially quotient of lengths, but need to take into account negative offsets 73 | // use dot product to find cosine of angle, should be 1 or -1 74 | let (sgn = (offv[0]*targetv[0] + offv[1]*targetv[1])/(norm(offv)*norm(targetv))) 75 | sgn * norm(targetv)/norm(offv) 76 | ] 77 | ; 78 | 79 | // transform polygon into sequence of edges as equations, skipping flipped edges 80 | // and then transform back into vertices by solving adjacent equations 81 | function _remove_edges(p, dist) = 82 | let (N = len(p), offlim = _offset_limit(p)) 83 | let (eqlist = [ for (i=[0:N-1]) 84 | if (!(dist/offlim[i] > 1)) 85 | _seg2eq(p[i], p[(i+1)%N], 0) 86 | ]) 87 | let (N2 = len(eqlist)) 88 | [ for (i=[0:N2-1]) 89 | _solve2eq(eqlist[(i+N2-1)%N2], eqlist[i]) ]; 90 | 91 | function _iterative_remove_edges(p, dist) = 92 | let (cleaned = _remove_edges(p, dist)) 93 | len(cleaned) == len(p) ? p : _iterative_remove_edges(cleaned, dist); 94 | 95 | // given a polygon, offset each vertex using the corner offset method above 96 | // note: this can produce self-intersections depending on the 'curvature' and offset 97 | function offset_poly(pts, dist) = 98 | _offset_poly(_iterative_remove_edges(pts, dist), dist); 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | ![Image of right_arrow](images/pathbuilder_welcome.png) 3 | 4 | Pathbuilder is a tool for openSCAD that make the creation of complex 2D shapes easier with a syntax similar to the one used for svg path. Pathbuilder supports [the complete svg command set](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands) and several extra commands to make the creation of 2d shapes as easy as possible. 5 | 6 | You can place fillets or Chamfers at any point. Extend lines up to an arbitrary boundary and much more. 7 | 8 | # Installation 9 | Although this library has grown to three code units, Pathbuilder is contained in a single .scad file and in has no dependencies on other libraries. Just copy pathbuilder.scad to your openSCAD library folder and put 10 | ``` 11 | use 12 | ``` 13 | at the top of your code unit and you are ready to go. Copy this code into a new blank openSCAD file to make sure it all works. Dont forget to check out the [demo folder](https://github.com/dinther/pathbuilder/tree/main/demo) it contains 6 examples on how Pathbuilder can be used. 14 | 15 | ### Quick test 16 | ``` 17 | include 18 | 19 | svgShape("m 0 0chamfer8h20fillet2v20fillet10h20v-10fillet2l35 20fillet2l-35 20fillet2v-10h-40fillet30", $fn=32); 20 | ``` 21 | Note that you can always add $fn or other global openSCAD parameters exclusivly into any function or module. 22 | You should see this. 23 | 24 | ![image](https://github.com/user-attachments/assets/f95b67f1-01f8-4429-8c9a-97c9d5168f60) 25 | 26 | # flexible 27 | 28 | There are two ways to use pathbuilder. You can use a normal svg path string in Pathbuilder. This means that you can also draw your shapes in vector drawing programs such as inkscape and copy the path string into Pathbuilder. 29 | 30 | However the power of openSCAD is of course in its parametric capabilities. Although you could string together a svg path string with some variables, it isn't exactly elegant. 31 | 32 | Pathbuilder also offers access to every command directly in your code. Commands are simply chained just like you are used to with openSCAD. A polygon is drawn at the end of the command sequence. This way you can pass your parameters directly into pathbuilder commands. 33 | 34 | ![image](https://user-images.githubusercontent.com/1192916/194191591-ef2b22b1-cdb1-4212-a432-21c76471f459.png) 35 | In the above example I used pathbuilder to generate complex ship hull shapes. Here I made extensive use of the svgPoints() function which returns a list of coordinates rather than letting pathbuilder produce the polygon. The points are then processed to produce the polyhedron in the picture. The mesh is perfect and my 3D printer slicer didn't report any issues. 36 | 37 | ## S bracket example 38 | 39 | ![image](https://user-images.githubusercontent.com/1192916/153122019-61eb8e5a-9a15-42e6-a4ba-5fbb8efa8b66.png) 40 | 41 | ### code 42 | 43 | ``` 44 | use 45 | 46 | // S bracket 47 | 48 | width = 40; 49 | length = 60; 50 | height = 30; 51 | thickness = 5; 52 | inner_radius = 3; 53 | 54 | $fn = 64; 55 | linear_extrude(width){ 56 | m(0,0) 57 | v(thickness) 58 | h((length - thickness)/2) 59 | fillet(inner_radius) 60 | V(height) 61 | fillet(inner_radius + thickness) 62 | H(length) 63 | v(-thickness) 64 | h(-(length - thickness)/2) 65 | fillet(inner_radius) 66 | V(0) 67 | fillet(inner_radius + thickness) 68 | H(0); 69 | } 70 | ``` 71 | Each method has its own benefits and drawbacks. The SVG path string method can create a polygon or return a point list which you can manipulate as desired. But modules also allow a much finer control over the curve segmentation as you can slip in a $fa, $fs or $fn parameter with every command module. 72 | 73 | # Rip and pillage 74 | If Pathbuilder isn't quite your thing you might still was to have a look at some of the functions. Pathbuilder has no dependencies and the functions have been written as self contained as possible and sensible. Even easier is to jump over to my other repository of stand-alone ready to use openSCAD functions called [openSCAD_functions](https://github.com/dinther/openSCAD_functions) 75 | 76 | Here are just a few examples: 77 | 78 | ``` 79 | function curveBetweenPoints(pt1, pt2, radius, incl_start_pt = true) 80 | ``` 81 | Returns a list of points representing the shortest curve between two points with a given radius. 82 | 83 | ``` 84 | function fillet(pts, index, radius) 85 | ``` 86 | Returns a list of points representing an arc with a given radius that is the tangent to pt1 and pt2 also known as a fillet. 87 | ``` 88 | function chamfer(pts, index, size) 89 | ``` 90 | Returns two points for a nice balanced symetrical chamfer of size for a given point in the point list. 91 | 92 | # Command overview 93 | Here is a quick overview of the commands implemented so far. Check out the detailed [command documentation in the wiki](https://github.com/dinther/pathbuilder/wiki) 94 | 95 | Most path commands have an uppercase and a lowercase version in Pathbuilder. This is an important distriction because the case defines if the command works in absolute coordinates `(Uppercase)` or relative `(lowercase)` from the current point. More about this in the wiki. In this overview I ignore the details case has on the commands. 96 | 97 | ## High level commands 98 | When using the SVG path syntax in string format you will need to call a function to have the commands in the string processed. The main two are at the top of this list. The other three are more useful as debugging tools. 99 | 100 | |Command|Code|Description| 101 | |-------|------|-------| 102 | |svgShape|`svgShape(path_string)`|This command takes a svg path string as input and creates a polygon with the defined shape. Segmentation of curves are according $fn, $fa, $fs and $pb_spline.| 103 | |svgPoints|`svgPoints(path_string)`|This command takes a svg path string as input and returns a 2D point list. Here you can do additional processing of your shape data.| 104 | |svgTweenPath|`svgTweenPath(path1, path2, factor)`|This command takes two similar svg path strings as input and returns a in between path string based on the factor value between 0 and 1. This is ideal to create complex morphing between curves which then can be used to create complex 3D meshes in the upcoming meshBuilder. Tweens are only possible when both SVG paths have the same command count and sequence.| 105 | |pb_tokenizeSvgPath|`pb_tokenizeSvgPath(path_string)`|Turns the path string into a list of unambiguous commands and returns this command_list. Useful for debugging.| 106 | |pb_processCommands|`pb_processCommands(command_list)`|Executes the commands in the list and builds up a point list and a post processing list. These two lists are returned in a data list.| 107 | |pb_postProcessPath|`pb_postProcessPath(data)`|Post processing involves applying fillets and chamfers now the main shape is known. A 2D point list is returned. 108 | 109 | Checkout [pathbuilderdemo.scad](pathbuilderdemo.scad) demo 3 which shows a third way to interpret process the svg path string. Here three separate steps are taken to go from path string to 2D point list. 110 | ``` 111 | pb_swoosh = "M68.56-4L18.4-25.36Q12.16-28 7.92-28q-4.8 0-6.96 3.36-1.36 2.16-.8 5.48t2.96 7.08q2 3.04 6.56 8-1.6-2.56-2.24-5.28-1.2-5.12 2.16-7.52Q11.2-18 14-18q2.24 0 5.04 .72z"; 112 | cmds = pb_tokenizeSvgPath(pb_swoosh); 113 | data = pb_processCommands(cmds); 114 | pts_list = pb_postProcessPathLists(data); 115 | pts = pts_list[0]; 116 | polygon(pts); 117 | ``` 118 | *** 119 | ## SVG Commands: 120 | The commands in this table should be fully compliant with the SVG path syntax. 121 | 122 | |Command|Code|Description| 123 | |-------|----|-------| 124 | |[M or m](https://github.com/dinther/pathbuilder/wiki/Move-to)|`"m x y"` or `m(x,y)` or
`m([x,y,...])`|Move must be the first command and sets a start point.| 125 | |L or l|`"l x y"` or `l(x,y)` or
`l([x,y,...])`|Line adds a point to the point path.| 126 | |H or h|`"h x"` or `h(x)` or `h([x,...])`|horizontal line to x.| 127 | |V or v|`"v y"` or `v(y)` or `v([y,...])`|vertical line to y.| 128 | |C or c|`"c cx1 cy1 cx2 cy2 x2 y2"` or
`c(cx1,cy1,cx2,cy2,x2,y2)` or
`c([cx1,cy1,cx2,cy2,x2,y2,...])`|Draws a cubic spline to x2,y2 where cx1,cy1 controls the entry angle/shape and cx2,cy2 controls the exit angle/shape.| 129 | |S or s|`"s cx cy x y"` or
`c(cx,cy,x,y)` or
`c([cx,cy,x,y,...])`|Draws a smooth cubic spline continuation to x,y where cx,cy controls the exit angle/shape. The entry control point comes from a prior cubic spline if there was one otherwise the entry angle/shape initially starts like a line to x,y.| 130 | |Q or q|`"q cx, cy, x, y"` or
`q(cx,cy,x,y)` or
`q([cx,cy,x,y,...])`|Draws a quadratic spline to x,y. Control point cx,cy is shared between the current point and x,y.| 131 | |T or t|`"t x y"` or `t(x,y)` or `t([x,y,...]`|Draws a smooth quadratic spline continuation to x,y using the control point from the previous quadratic spline. This sequence must start with a regular quadratic spline otherwise you get straight lines.| 132 | |A or a
(not ready)|`"a rx ry a lf sf x y"` or
`a(rx,ry,a,lf,sf,x,y)` or
`a([rx,ry,a,lf,sf,x,y,...])`|Drawn an arc of a ellipse segment to x,y with radii rx and ry with the ellipse rotated to angle a. lf and sf flags select from 4 possible solutions. lf short way (0) or long way(1) and sf: cw (0) or ccw (1)| 133 | *** 134 | ![Image of right_arrow](images/ExtendToBoundary.png)
Example of using the forward command with a polyline boundary 135 | 136 | ## Extra path commands: 137 | These are extra commands introduced by Pathbuilder. The command set is not settled yet. In fact, only fillet and chamfer are likely to remain as they are. angle, polar and forward could be rolled into a single command. 138 | |Command|Code|Description| 139 | |-------|----|-------| 140 | |Angle or angle|`"angle a"` or `angle(a)`|Changes the currentexit angle from the last command.| 141 | |Forward or forward|`"forward d ..."` or
`forward(d)` or
`forward([x1,y1,x2,y2...]`|Extends a point in the direction of the current exit angle. This point can be at distance d or until a polyline is intersected formed by x,y value pairs.| 142 | |Polar or polar|`"polar d a"` or `polar(d,a)`|Draws a line to a point d distance away and angle a.| 143 | |Segment or segment|`"segment x y r"` or `segment(x,y,r)` or `segment([x1,y1,x2,y2...],r)`|Draws the shortest circle segment between current point and x,y with radius r. Make r negative to change the curve from CW to CCW or vice versa. **depricated**| 144 | |fillet|`"fillet r"` or `fillet(r)`| Replaces the current point with a circle segment of radius r. The curve is placed tangential to the entry and exit lines which must be long enough to accomodate the fillet curve. Flip the curve by making r negative.| 145 | |chamfer|`"chamfer s"` or `chamfer(s)`|Replaces the current point with a symetrical chamfer of size s. The entry and exit lines must be long enough to accomodate the chamfer| 146 | 147 | -------------------------------------------------------------------------------- /pointutils.scad: -------------------------------------------------------------------------------- 1 | // pointutils.scad 2 | // 3 | // This utility libray is designed to help with the generation of complex 3D point lists 4 | // Amazing results can be achieved when used in combination with Pathbuilder and MeshBuilder 5 | // 6 | // Latest version here: https://github.com/dinther/pathbuilder 7 | // 8 | // By: Paul van Dinther 9 | 10 | // function rectPoints(rect=[], div=[], z=undef, center=false) 11 | // 12 | // Generates a list of points describing the requested rectangle. The number of points per size can be controlled. List is 2D if rect contains 2 values otherwise 3D. 13 | // rect (vector) 2D or 3D Defines the size of the rectangle. 14 | // return (list) List of 2D or 3D points forming a rectangle. 15 | function rectPoints(rect=[1,1], center=false) = let( 16 | x=center? -rect[0] * 0.5 : 0, 17 | y=center? -rect[1] * 0.5 : 0, 18 | pts = [[0+x,0+y],[0+x,rect[1]+y],[rect[0]+x,rect[1]+y],[rect[0]+x, 0+y]]) rect[2]==undef? pts : appendValueToPoints(pts, rect[2]); 19 | 20 | // function circlePoints(r=undef, d=undef, z=undef) 21 | // 22 | // Generates a list of 2D points describing the requested circle. The number of points will depend 23 | // on the usual openSCAD $fa, $fs, $fn settings. 24 | // will be a 3D point. 25 | 26 | // r (number) Radius of the circle. 27 | // d (number) Diameter of the circle. 28 | // z (number) Z coordinate optional. Causes a 3D point list to be returned. 29 | // return (list) List of 2D or 3D points forming a circle. 30 | 31 | function circlePoints(r=undef, d=undef, z=undef) = let( 32 | _r = r!=undef? r : d!=undef? d*0.5 : 1, 33 | _c = _pu_segmentsPerCircle(_r), 34 | _s = 360 / _c 35 | ) [for(i=[0:_c-1]) let(_cos = cos(i*_s), _sin = sin(i*_s)) z==undef? [_cos * _r, _sin * _r,] : [_cos * _r, _sin * _r, z]]; 36 | 37 | // function appendValueToPoints(pts, value) 38 | // 39 | // Appends a value to every point in the list. Typically used when you want to turn a 2D point list into a 3D point list. 40 | 41 | // pts (list) List of points. 42 | // value (number) Value to be apended to every point in the list. 43 | // return (point) Resulting list of points with the extra value. 44 | function appendValueToPoints(pts, value) = [for(pt=pts) concat(pt,[value])]; 45 | 46 | // function keep(dataList=[], indexes = []) 47 | // 48 | // Takes any list of data and only returns the fields given by the indexes. This function is also perfect when you want to reorder data. 49 | // Turn 3D points lists into 2D point lists etc. A warning will be given when an index exceeds the number of data items. 50 | // 51 | // dataList (list) List of any kind of data of any length. For example mixed 2D and 3D points. 52 | // indexes (list) List of indexes for the required fields. 53 | // return (point) Resulting organized list 54 | // 55 | // Example keep(dataList=[[0,0,3],[2,10,0],[12,10]], indexes=[1,0]); // Produces list with only y,x coodinates [[0, 0], [10, 2], [10, 12]] 56 | 57 | function keep(dataList=[], indexes = [], _i=0, _data=[]) = let( 58 | a = assert(max(indexes) < len(dataList[_i]), str("Index ",max(indexes)," in indexes list is too large for given data ",dataList[_i])), 59 | d = [for(i=indexes) dataList[_i][i]], 60 | _data = concat(_data, [d])) _i=len(pt)? defaults[i] : pt[order[i]]] ]; 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | // ********************************************************************************************************************************* 213 | // Helper functions 214 | 215 | 216 | // function sp(r) 217 | // 218 | // SP (Show points) to quickly visualise a list of points. 219 | // 220 | // pts (list) List of 2D or 3D points. 221 | // r (number) Radius of the point markers. 222 | // color (string) Color of the point markers. 223 | module sp(pts=[], r=1, color="yellow"){ 224 | for(pt=pts) translate(pt) color(color) sphere(r=r); 225 | } 226 | 227 | // function _pu_segmentsPerCircle(r) 228 | // 229 | // Calculates the number of segments used per circle (per 360 deg) based on circle radius and openSCAD $fn, $fa and $fs settings. 230 | // r (number) The circle radius for which the number of segments needs to be calculated. 231 | // return (number) The number of segments that would be used by openSCAD if it would draw the circle. 232 | function _pu_segmentsPerCircle(r=1) = $fn>0?($fn>=3?$fn:3):ceil(max(min(360/$fa,abs(r)*2*PI/$fs),5)); 233 | 234 | // function distanceBetween calculates the distance between two 2D or 3D points 235 | function distanceBetween(pt1, pt2) = sqrt(pow(pt2[0] - pt1[0], 2) + pow(pt2[1] - pt1[1], 2) + pow(pt2[2]==undef? 0 : pt2[2] - pt1[2]==undef? 0 : pt1[2], 2)); 236 | 237 | // function lengthPoints(pts=[]) 238 | // 239 | // Calculates the length of a point list by adding up the distances between all points. 240 | // this can be 2D or 3D points. 241 | // pts (list) 2D or 3D point list 242 | // return (number) Total length of the path. 243 | function lengthPoints(pts=[], _i=0, _sum=0) = _i 1)) 301 | _os_seg2eq(p[i], p[(i+1)%N], 0) 302 | ]) 303 | let (N2 = len(eqlist)) 304 | [ for (i=[0:N2-1]) 305 | _os_solve2eq(eqlist[(i+N2-1)%N2], eqlist[i]) ]; 306 | 307 | function _os_iterative_remove_edges(p, dist) = 308 | let (cleaned = _os_remove_edges(p, dist)) 309 | len(cleaned) == len(p) ? p : _os_iterative_remove_edges(cleaned, dist); 310 | 311 | // given a polygon, offset each vertex using the corner offset method above 312 | // note: this can produce self-intersections depending on the 'curvature' and offset 313 | function offset_poly(pts, dist) = 314 | _offset_poly(_iterative_remove_edges(pts, dist), dist); 315 | -------------------------------------------------------------------------------- /pathbuilder.scad: -------------------------------------------------------------------------------- 1 | // pathbuilder 2 | // 3 | // This libray is designed to create complex 2D shapes 4 | // It uses commands similar to the svg path syntax 5 | // 6 | // examples 7 | // 8 | // in code: 9 | // linear_extrude(1) m(0,0) chamfer(10) h(20) fillet(2) v(10) segment(10, 10, 10) h(10) fillet(2) v(-10) fillet(2) l(35,20) fillet(2) L(40, 50) fillet(2) v(-10) h(-10) Segment(0,10,-30) draw(); 10 | // 11 | // or SVG path syntax: 12 | // 13 | // 14 | // The above statement creates a 2D right angle arrow that is then extruded 15 | // Lowercase commands are drawn relative to the last drawn point. 16 | // Uppercase commands are drawn as absolute coordinates. 17 | // 18 | // Latest version here: https://github.com/dinther/pathbuilder 19 | // 20 | // By: Paul van Dinther 21 | 22 | 23 | // Global values 24 | 25 | // $pb_spline 26 | // This value controls how many line segments are created for all types of spline curves. 27 | $pb_spline = 10; 28 | 29 | // function svgPoints(s) 30 | // 31 | // Processes a SVG path string and returns a 2D point list. This allows user point manipulation before the points are used. 32 | // path (list) String compliant with SVG path syntax plus the extra commands introduced in pathBuilder. 33 | // return (list) List of lists of 2D points that each outline the intended SVG path. Can be directly consumed by the polygon command. 34 | function svgPoints(path, z=undef) = let( 35 | pointlists = pb_postProcessPathLists(pb_processCommands(pb_tokenizeSvgPath(path))) 36 | ) z==undef? pointlists : [for (pts=pointlists) pb_translate_pts(pts,[0,0,z])]; 37 | 38 | // module svgShape(s) 39 | // 40 | // Processes a SVG path string and returns a 2D polygon. 41 | // path (list) String compliant with SVG path syntax plus the extra commands introduced in pathBuilder. 42 | // return (polygon) polygon can be further handled by any openSCAD command. 43 | module svgShape(path="", _i=-2, _p=undef, _first_CW=undef, $pb_spline=10){ 44 | _p = _p==undef? svgPoints(path) : _p; 45 | l = len(_p); 46 | _first_CW = _i<-1? pb_is_CW(_p[0]) : _first_CW; 47 | _i = _i==-2? l-1 : _i; 48 | if (l>0){ 49 | if (l==1) polygon(_p[0]); 50 | if (_i>=0){ 51 | CW2 = pb_is_CW(_p[_i]); 52 | if (CW2 == _first_CW) union(){ 53 | polygon(_p[_i]); 54 | svgShape(path, _i-1, _p, _first_CW); 55 | } else difference(){ 56 | svgShape(path, _i-1, _p, _first_CW); 57 | polygon(_p[_i]); 58 | } 59 | } 60 | } 61 | } 62 | 63 | 64 | 65 | // function svgTweenPath(s) 66 | // 67 | // Processes two similar paths where the command sequence is identical but the parameters are different. 68 | // The return value is a path string that is either Path1, Path2 or somewhere in between depending on the factor value 69 | // which must be somewhere between 0 and 1. This function is great to create in between splines. 70 | // path1 (list) String compliant with SVG path syntax plus the extra commands introduced in pathBuilder. 71 | // path2 (list) String compliant with SVG path syntax plus the extra commands introduced in pathBuilder. 72 | // factor (number) Number between 0 and 1 determining the path values of the return path. 73 | // return (list) String compliant with SVG path syntax plus the extra commands introduced in pathBuilder. 74 | function svgTweenPath(path1="", path2="", factor=0) = let( 75 | f = max(0, min(1, factor)), 76 | commandList1 = pb_tokenizeSvgPath(path1), 77 | commandList2 = pb_tokenizeSvgPath(path2), 78 | a = assert(len(commandList1) == len(commandList2), "path1 and path2 must have an equal number of commands."), 79 | commandList = [for (i=[0:1:len(commandList1)-1]) let( 80 | a = assert(commandList1[i][0] == commandList2[i][0], "Command mismatch. The command sequence of path1 and path2 must be identical.") 81 | ) [commandList1[i][0], 82 | len(commandList1[i][1])==0? [] : [for(j=[0:len(commandList1[i][1])-1]) let( 83 | v1 = commandList1[i][1][j], 84 | v2 = commandList2[i][1][j] 85 | ) v1 + ((v2-v1) * f)]]] 86 | ) pb_commandListToPath(commandList); 87 | 88 | 89 | 90 | // Helper functions: 91 | 92 | // function pb_angle(p1,p2) 93 | // 94 | // Calculates the angle of line between vector v1 and v2 95 | // v1 (list) 2D vector with x and y value. 96 | // v1 (list) 2D vector with x and y value. 97 | // return (number) Angle between the two vectors in degrees. 98 | function pb_angle(v1, v2) = let(a = atan2(v2[0] - v1[0], v2[1] - v1[1])) a; 99 | 100 | // function pb_angle(p1,p2) 101 | // 102 | // Calculates the angle of the last line segment in the points list pts. 103 | 104 | // Returns 0 when the point list only has one value in it. 105 | // pts (list) List of 2D points. 106 | // angle (number) Last known angle. Returned in case pts is empty 107 | // return (number) Angle of the last line segment in the list. 108 | function pb_calcExitAngle(pts=[],angle) = let( 109 | l = is_list(pts)? len(pts) : -1, 110 | p1=l>1? pts[l-2] : [0,0], 111 | p2=l>0? pts[l-1] : undef) p2==undef || p1==p2? angle==undef? 0 : angle : pb_angle(p1, p2); 112 | 113 | // pb_reflectPntOn(p1, pc) 114 | // 115 | // Rotates p1 180 degrees around pc. All points are absolute coordinates. 116 | // p1 (list) List of two numbers representing the 2D point to be reflected. 117 | // pc (list) (default=[0,0]) List of two numbers representing the 2D center point around which p1 is rotated 118 | // return (list) List of two numbers representing the new rotated point. 119 | function pb_reflectPntOn(p1=[], pc=[0,0]) = pc + (p1-pc)*-1; 120 | 121 | // pb_translate_pts(pts, translate) 122 | // 123 | // translates points in point list. 124 | // pts (list) List of 2D or 3D points. 125 | // translate (list) List of three or two numbers representing the 3D vector to translate the points with 126 | // return (list) List of translated 3D points. 127 | function pb_translate_pts(pts, translate=[0,0,0])= [for(p=pts)[p[0]+translate[0],p[1]? p[1]+translate[1] : translate[1], p[2]? p[2]+translate[2] : translate[2]]]; 128 | 129 | 130 | // function pb_segmentsPerCircle(r) 131 | // 132 | // Calculates the number of segments used per circle (per 360 deg) based on circle radius and openSCAD $fn, $fa and $fs settings. 133 | // r (number) The circle radius for which the number of segments needs to be calculated. 134 | // return (number) The number of segments that would be used by openSCAD if it would draw the circle. 135 | function pb_segmentsPerCircle(r=1) = $fn>0?($fn>=3?$fn:3):ceil(max(min(360/$fa,abs(r)*2*PI/$fs),5)); 136 | 137 | // function pb_last(pts) 138 | // 139 | // Returns last item from the list. If the list is empty it will return [0,0] 140 | // pts (list) List of zero or more 2D points. 141 | // return (list) List of two numbers which is a 2D point. 142 | function pb_last(pts) = let(l=is_list(pts)? len(pts) : 0) l==0? [0,0] : pts[l-1]; 143 | // function pb_subList(list, start, end) 144 | // 145 | // Returns subset of list defined by start and end which are indexes to the list. Indexes are zero based. 146 | // When indexes are negative they refer from the end of the list rather than the start. 147 | // list (list) List of arbitrary data. 148 | // start (number) (default = 0) index from the start where items need to be included in the returned list. Negative numbers represent an index from the end of the list. 149 | // end (number) (default = list length - 1) index from the start after which items no longer need to be included. Negative numbers represent an index from the end of the list. 150 | // return (list) sub list of items from the input list. Items can appear reversed when end references an index that is less than the start index. 151 | // 152 | // examples: 153 | // pb_subList([0,1,2,3,4,5,6]) => [0,1,2,3,4,5,6] // copies the list 154 | // pb_subList([0,1,2,3,4,5,6],3) => [3,4,5,6] // starts at index 3 up to the end 155 | // pb_subList([0,1,2,3,4,5,6],3, 4) => [3,4] // starts at index 3 up to end index 4 156 | // pb_subList([0,1,2,3,4,5,6],3, 2) => [3,2] // starts at index 3 down to end index 2 157 | // pb_subList([0,1,2,3,4,5,6],-2) => [5,6] // starts 2 before the end of the list up to the end 158 | // pb_subList([0,1,2,3,4,5,6],-3, -4) => [4,3] // starts 3 before the end of the list down to 4 before the end of the list 159 | // pb_subList([0,1,2,3,4,5,6],-1, 0) => [6,5,4,3,2,1,0] // Reverse list 160 | function pb_subList(list, start=0, end) = let(l = len(list), s = start<0? l+start: start, e = end==undef? l-1 : end<0? l+end: end, sp=elen(list)-1? _s : pb_valuesToString(list, seperator, _i+1, str(_s, list[_i],_i==len(list)-1? "" : seperator)); 169 | 170 | 171 | 172 | // function pb_removeAdjacentDuplicates(s) 173 | // 174 | // Removes adjacent duplicate points 175 | // pts (list) List of points. 176 | // testLastToFirst (boolean) Set to false if you want to skip testing last point to first point. 177 | // return (string) String containing all items from the list. 178 | function pb_removeAdjacentDuplicates(pts=[], testLastToFirst=true, _i=0, _r=[]) = let( 179 | l = len(pts), _n = testLastToFirst? (_i==l-1? 0: _i+1) : _i+1 180 | ) _i 2 ? str(jb(b,m), jb(m,e)) : s == 2 ? str(l[b],l[b+1]) : l[b], 189 | jd = function (b,e) let(s = e-b, m = floor(b+s/2)) // join delimiter 190 | s > 2 ? str(jd(b,m), d, jd(m,e)) : s == 2 ? str(l[b],d,l[b+1]) : l[b]) 191 | s > 0 ? (d=="" ? jb(0,s) : jd(0,s)) : ""; 192 | 193 | function substr(s,b,e) = let(e=is_undef(e) || e > len(s) ? len(s) : e) (b==e) ? "" : join([for(i=[b:1:e-1]) s[i] ]); 194 | 195 | function split(s,separator=" ") = separator=="" ? [for(i=[0:1:len(s)-1]) s[i]] : 196 | let(t=separator, e=len(s), f=len(t), 197 | _s=function(b,c,d,r) b0 || (remain>0&&!only_groups) ? [for(m=[l-remain-drop:l-1]) list[m]] : []] ) result; 241 | 242 | // function pb_is_CW(pts) 243 | // 244 | // Tests if pts forms a Clock Wise (CW) or Counter Clock Wize (CCW) winding 245 | // pts (list) A list of 2D points assumed a closed polyline. 246 | // return (bool) true if pts is a wound Clock Wize otherwise false. 247 | function pb_is_CW(pts=[], _i=0, _r=0) = _i==len(pts)? _r>0 : pb_is_CW(pts, _i+1, _r + cross(_i==len(pts)-1? pts[0] : pts[_i+1], pts[_i])); 248 | 249 | // function pb_is_2d(p) 250 | // 251 | // Tests if the value p is a 2D point. 252 | // p (list) A list of two numbers representing a 2D point. 253 | // return (bool) true if p is a 2D point otherwise false. 254 | function pb_is_2d(p=[]) = is_list(p) && len(p)==2 && is_num(p[0]) && is_num(p[1]); 255 | 256 | // function is_between(v1, v, v2) 257 | // 258 | // Tests if value v is between or equal to v1 and v2. It does not matter if v1 is greater than v2 or vice versa. 259 | // v1 (number or list) Defines one of the two value boundaries. Value can be a number or a list representing a 2D point. 260 | // v (number or list) Defines one value to be tested against v1 and v2. It does not matter 261 | // v2 (number or list) Defines one of the two value boundaries. Value can be a number or a list representing a 2D point. 262 | // return (bool) 263 | // false if the value v is outside the boundaries v1 and v2. 264 | // true if the value is inside the boundaries v1 and v2. 265 | function pb_is_between(v1, v, v2) = let( p=is_num(v)? [v,0]: v, p1=is_num(v1)? [v1,0]: v1, p2=is_num(v2)? [v2,0]: v2, d = p2-p1, 266 | i=abs(d[0])>abs(d[1])? 0:1, a = p[i]>= min(p1[i],p2[i])&& p[i]<=max(p1[i],p2[i])) a; 267 | 268 | // pb_intersectLineWithPolyline(line, pts, mode, all, sort) returns list with points if any 269 | // 270 | // calculates the intersections between a line and polyline. 271 | // line (list) A list of two 2D points. eg [[0,0],[0,5]] 272 | // pts (list) A list of one or more 2D points forming a polyline. Polyline does not self close. A a single point is converted into three points, forming one horizontal and one vertical line. 273 | // all (bool) 274 | // false (default) stops looking for intersections once a valid one is found. sort has no effect. 275 | // true builds a list of all valid intersections. The result can be sorted. 276 | // sort (list) (default = []) The intersection results will be sorted by distance to the 2D point that can be provided here. 277 | // false The list of valid intersections is returned in the order they were found. 278 | // true (default) The list of valid intersections is sorted based on proximity to the line end point. Nearest is at the top of the list. 279 | // on_line (bool) On line result filter. 280 | // undef (default) no intersections are filtered out. 281 | // false only intersections that are not on line are returned. 282 | // true only intersections that are on line are returned. 283 | // on_pts (bool) On pts polyline result filter. 284 | // undef (default) no intersections are filtered out. 285 | // false only intersections that are not on the pts polyline are returned. 286 | // true only intersections that are on the pts polyline are returned. 287 | // return (list) A list of intersection results. Each item represents an intersection. An empty list is returned if there are no intersections. 288 | // return[n] Data block representing one intersection 289 | // return[n][0] (list) A list of two numbers representing a 2D point where the intersection was found. 290 | // return[n][1] (bool) Intersection found on line. true when it was found on the line otherwise false. 291 | // return[n][2] (bool) Intersection found on polyline segment. true when it was found on a line segment, otherwise false. 292 | // return[n][3] (number) index of polyline segment if the intersection was found on a polyline otherwise -1. 293 | // return[n][4] (number) Distance from the the intersection to the sort point if one was provided. Otherwize value is -1. This value is used by the sort routine. 294 | function pb_intersectLineWithPolyline(line=[], pts=[],all=false, sort=[], on_line=undef, on_pts=undef, _i=0, _r=[]) = 295 | (!all&&len(_r)>0) ||_i==len(pts)-1? pb_is_2d(sort)? _pb_intersect_sort(_r,3) : _r : let( 296 | da = line[0]-line[1], db = pts[_i] - pts[_i+1], the = cross(da,db), 297 | d = (the == 0)? [] : let( A = cross(line[0], line[1]), B = cross(pts[_i], pts[_i+1]), 298 | x=( A*db[0] - da[0]*B ) / the, y=( A*db[1] - da[1]*B ) / the, a=[x,y], 299 | ol = pb_is_between(line[0] ,[x,y] ,line[1]), 300 | op = pb_is_between(pts[_i], [x,y] ,pts[_i+1]), 301 | p = [a, ol, op, pb_is_2d(sort)? norm(a-sort):-1, op?_i:-1] ) p, 302 | a = pb_intersectLineWithPolyline(line, pts, all, sort, on_line, on_pts, _i+1, d==[]||d==pb_last(_r)? _r : concat(_r, [d])) 303 | ) [for(i=a) if ((on_line==undef || on_line==i[1]) && (on_pts==undef || on_pts==i[2])) i]; 304 | 305 | // function _pb_intersect_sort(list) 306 | // 307 | // Sorts intersection data produced by pb_intersectLineWithPolyline from near to far. 308 | // Function is used internally but can be used externally. 309 | // list (list) List of intersection data. See pb_intersectLineWithPolyline for structure details. 310 | // return (list) List of intersection data sorted from near to far 311 | function _pb_intersect_sort(list, sort_idx=2) = 312 | len(list)<=1 ? list : let( 313 | pivot = list[floor(len(list)/2)][sort_idx], 314 | lesser = [ for (d = list) if (d[sort_idx] < pivot) d ], 315 | equal = [ for (d = list) if (d[sort_idx] == pivot) d ], 316 | greater = [ for (d = list) if (d[sort_idx] > pivot) d ] 317 | ) concat( _pb_intersect_sort(lesser, sort_idx), equal, _pb_intersect_sort(greater, sort_idx) ); 318 | 319 | 320 | // function pb_parseNum(s) 321 | // 322 | // Converts a string into a number. this can be an integer or a floating point variable. 323 | // The function can not handle hex notation. 324 | // s (list) String representing a number. Valid characters are +-0123456789e and . 325 | // return (number) Can be either integer positive or negative or floating point value positive or negative 326 | function pb_parseNum(s, _i=0, _n=0, _d=0, _r1=0, _r2=0) = _i==len(s)? s[0]=="-"? -(_r1+_r2) : _r1+_r2 : let( 327 | o = ord(s[_i]), f = o==101? pb_parseNum(pb_substring(s, _i+1, len(s))) : 0, _n = o==45 || o==43? 1: _n, 328 | _d = o == 46? _i+1 : _d, c = (o>47 && o<58), _r1 = c&&_d==0? _r1*10+(o-48) : _r1, _r2 = c&&_d!=0? _r2+(o-48) * pow(10,-_i+_d-1) : _r2 329 | ) pb_parseNum(pb_replacechar(s), f==0? _i+1 : len(s), _n, _d, _r1, _r2) * pow(10,f); 330 | 331 | // function pb_commandListToPath(commandList) 332 | // 333 | // Converts command list into a path string. 334 | // commandList (list) List of SVG and Pathbuilder commands. Each item consists of a command identifier and a number list. 335 | // return (string) String compliant with SVG path syntax plus the extra commands introduced in pathBuilder. 336 | function pb_commandListToPath(commandList = []) = 337 | pb_valuesToString([for(command = commandList) str(command[0],pb_valuesToString(command[1]))],""); 338 | 339 | // function pb_tokenizeSvgPath(s) 340 | // 341 | // Parses a path string and turns it into a command list. 342 | // s (list) String compliant with SVG path syntax plus the extra commands introduced in pathBuilder. 343 | // return (list) Command list 344 | // command (string) Command identifier 345 | // values (list) Values associated with the command 346 | // value (number) Value associated with the command 347 | function pb_tokenizeSvgPath(s, _i=0, _cmds=[], _cmd=[], _w = "", _d=0) = 348 | _i>len(s)-1? _cmds : let( 349 | l=len(s), c1 = s[_i], a1 = ord(c1), a2 = ord(s[min(l-1,_i+1)]), _d = a2==46? _d+1 : _d, 350 | // 0=number 1=sign 3=sep 4=dot 5=exp 2=char 351 | t1 = a1>47 && a1<58? 0 : a1==43 || a1==45? 1 : a1==32 || a1==44? 3 : a1==46? 4 : (a1==101 || a1==69)? 5 : 2, 352 | // 6=end 0=number 1=sign 3=sep 4=dot 5=exp 2=char 353 | t2 = _i==l-1? 6 : (a2>47 && a2<58)? 0 : a2==43 || a2==45? 1 : a2==32 || a2==44? 3 : a2==46? 4 : (a2==101 || a2==69)? 5 : 2, 354 | c = 355 | t1==2&&t2==0? 1 : // char to num "m 0 0 cha 356 | t1==2&&t2==3? 2 : // char to sep 357 | t1==2&&t2==1? 3 : // char to sign 358 | t1==2&&t2==4? 4 : // char to dot 359 | t1==2&&t2==6? 5 : // char to end 360 | t1==0&&t2==2? 6 : // num to char 361 | t1==0&&t2==3? 7 : // num to sep 362 | t1==0&&t2==1? 8 : // num to sign 363 | t1==0&&t2==6? 9 : // num to end 364 | t1==0&&t2==4&&_d>1? 11 : // num to next dot 365 | t1==5&&t2==0? 0 : // exp to num 366 | t1==5&&t2==1? 0 : // exp to sign 367 | 0, // not important 368 | dc = c==6 || c==7 || c==8 || c==9? 0 : _d, 369 | //w = t1!=3&&(c1!="z" && c1!="Z)? str(_w,c1) : _w, 370 | w = t1!=3? str(_w,c1) : _w, 371 | //w = t1!=3? str(_w,c1) : _w, 372 | _cmd = c>0||t2==6? t1==2? [w,[]] : [_cmd[0],concat(_cmd[1],[pb_parseNum(w)])] : _cmd, 373 | _cmds = (t1==0 || t1==3 || t2==6) && (t2==2|| t2==6)? concat(_cmds, [_cmd]) : _cmds, 374 | _w = c>0? "" : w 375 | )pb_tokenizeSvgPath(s=s, _i=_i+1, _cmds=_cmds, _cmd=_cmd, _w=_w, _d=dc); 376 | 377 | 378 | // function checks command list and splits command list to multiple command lists for every m or M command 379 | // 380 | function pb_splitCommandLists(cmds=[], _i=0, _p=[], _r=[]) = _i==len(cmds)? _r : let( 381 | n = cmds[_i][0] =="m" || cmds[_i][0] =="M"? true : false, 382 | _r = n && _p!=[]? concat(_r, [_p]) : _r, 383 | _p = n? [cmds[_i]] : concat(_p,[cmds[_i]]), 384 | r = _i==len(cmds)-1? concat(_r, [_p]) : _r 385 | ) pb_splitCommandLists(cmds, _i+1, _p, r); 386 | 387 | function pb_processCommandLists(cmds_list) = [for (cmds=cmds_list) pb_processCommands(cmds)]; 388 | 389 | // function pb_processCommands(cmds) 390 | // 391 | // Processes all the commands in the command list and generates a preliminary 2D point list and a post process command list 392 | // When finished, the point list contains all points except fillet, chamfer and z. Those are applied in a seprate pb_postProcessPath command. 393 | // cmds (list) List of path commands 394 | // return (list) Intermediate 2D points list and post processing list 395 | // return[0] (list) List with 2D points. 396 | // return[1] (list) List with post process commands such as fillet and chamfer which are applied at the end. 397 | // return[2] (number) current angle in degrees 398 | // return[3] (list) List containing 2 bezier spline control points. However, only one can be set at a time and the other should be empty. 399 | // return[3][0] (list) 2D point representing the control point of the last command which must have been a quadratic spline type. Otherwise empty ([]) 400 | // return[3][1] (list) 2D point representing the control point of the last command which must have been a cubic spline type. Otherwise empty ([]) 401 | 402 | function pb_processCommands(cmds=[], _i=0, _r=[[],[],0,[[],[]]], _f=[]) = 403 | assert(is_list(cmds) && len(cmds) > 0 && (cmds[0][0] == "m" || cmds[0][0] == "M"), str("cmds must be a list and start with a M (move) command but started with ",cmds[0][0])) 404 | _i==len(cmds)? concat(_f,[[_r[0],concat(_r[1],[[4,len(_r[0])-1]])]]) : let( 405 | cmd = cmds[_i], 406 | o = ord(cmd[0]), 407 | c = cmd[0], 408 | a = _r[2], 409 | ctl = _r[3], 410 | l = pb_last(_r[0]), 411 | d = c=="m"? _pb_line(_r[0], true,cmd[1],a,true) : 412 | c=="M"? _pb_line(_r[0], false, cmd[1],a,true) : 413 | c=="l"? _pb_line(_r[0], true, cmd[1], a, false) : 414 | c=="L"? _pb_line(_r[0], false, cmd[1], a, false) : 415 | c=="h"? _pb_horz(l, cmd[1], true, a) : 416 | c=="H"? _pb_horz(l, cmd[1], false, a) : 417 | c=="v"? _pb_vert(l, cmd[1], true, a) : 418 | c=="V"? _pb_vert(l, cmd[1], false, a) : 419 | c=="c"? _pb_cubic(l, cmd[1], true, a) : 420 | c=="C"? _pb_cubic(l, cmd[1], false, a) : 421 | c=="s"? _pb_smooth_cubic(l, cmd[1], true, a, ctl) : 422 | c=="S"? _pb_smooth_cubic(l, cmd[1], false, a, ctl) : 423 | c=="q"? _pb_quadratic(l, cmd[1], true, a) : 424 | c=="Q"? _pb_quadratic(l, cmd[1], false, a) : 425 | c=="t"? _pb_smooth_quadratic(l, cmd[1], true, a, ctl) : 426 | c=="T"? _pb_smooth_quadratic(l, cmd[1], false, a, ctl) : 427 | c=="a"? _pb_arc(l, cmd[1], true, a) : 428 | c=="A"? _pb_arc(l, cmd[1], false, a) : 429 | c=="z" || c=="Z"? _pb_close(_r[0], a) : 430 | c=="polar"? _pb_polar(_r[0], [cmd[1][0], cmd[1][1]+a]) : 431 | c=="Polar"? _pb_polar(_r[0], cmd[1]) : 432 | c=="forward"? _pb_forward(l, cmd[1], true, a) : 433 | c=="Forward"? _pb_forward(l, cmd[1], false, a) : 434 | c=="angle"? _pb_angle(cmd[1], false, a) : 435 | c=="Angle"? _pb_angle(cmd[1], true, a) : 436 | c=="segment"? _pb_segment(last=l, args=cmd[1], rel=true) : 437 | c=="Segment"? _pb_segment(last=l, args=cmd[1], rel=false) : 438 | c=="fillet"? _pb_fillet(_r[0], cmd[1], a) : 439 | c=="chamfer"? _pb_chamfer(_r[0], cmd[1], a) : [], 440 | _f = (c=="m" || c=="M") && _r[0]!=[]? concat(_f, [[_r[0],concat(_r[1],[[4,len(_r[0])-1]]),_r[2],_r[3]]]) : _f, 441 | r = c=="m" || c=="M"? d : d==[]? _r : [concat(_r[0], d[0]),concat(_r[1], d[1]), is_num(d[2])? d[2] : _r[2],d[3]] 442 | ) pb_processCommands(cmds, _i+1, c=="m" || c=="M"? d : d==[]? _r : [concat(_r[0], d[0]),concat(_r[1], d[1]), is_num(d[2])? d[2] : _r[2],is_list(d[3])? d[3]: [[],[]]], _f); 443 | 444 | 445 | // Applies fillets and chamfer commands to the raw point list 446 | // chamfer = 2 447 | // fillet = 3 448 | // z = 4 449 | 450 | function pb_postProcessPathLists(data_list =[]) = [for (data=data_list) 451 | let( 452 | pts = pb_removeAdjacentDuplicates(data[0]), 453 | steps = data[1], 454 | l = len(steps), 455 | first_pt = steps[1][1]==0? [] : [pts[0]], 456 | last_pt = steps[l-2][1]==len(pts)-1? [] : [pts[len(pts)-1]], 457 | result = [for (i = [0: len(steps)-2]) let( 458 | step = data[1][i], 459 | next_step = data[1][i+1], 460 | start = step[1], 461 | end = next_step[1], 462 | fill = (step[0]==2)? pb_fillet(pts, step[1], step[2], step[3]) : [], 463 | chamf = (step[0]==3)? pb_chamfer(pts, step[1], step[2]) : [], 464 | post = start+1<=end-1?pb_subList(pts, start+1, end-1) : [] 465 | ) for (p=concat(fill, chamf, post)) p], 466 | r = concat(first_pt, result, last_pt), 467 | rr = concat(r, len(steps)>0 && pb_last(steps)[0]==4? [r[0]] : []) 468 | ) rr]; 469 | 470 | 471 | // Calculates tangent fillet for any given point in a closed points list. Flip the curve by setting the radius negative. 472 | // pts (list) List of 2D points. 473 | // index (number) Index to the point for which a fillet is required. 474 | // radius (number) Radius for the requested fillet 475 | // segments(number) Optional fixed number of segments to draw the fillet. 476 | // return (segments) List of points representing the fillet curve that can replace the given point by index. 477 | function pb_fillet(pts, index, radius, segments=0) = let( 478 | a = index==0? pts[len(pts)-1] : pts[index-1], 479 | b = pts[index], 480 | c = index == len(pts)-1? pts[0]:pts[index+1], 481 | ba = a-b, 482 | bc = c-b, 483 | l1 = norm(ba), 484 | l2 = norm(bc), 485 | cos_angle = ba * bc / (l1 * l2), 486 | tan_half_angle = sqrt((1 - cos_angle) / (1 + cos_angle)), 487 | bf_length = abs(radius) / tan_half_angle, 488 | ba_u = ba/l1, 489 | bc_u = bc/l2, 490 | bf = ba_u*bf_length, 491 | bg = bc_u*bf_length, 492 | f = b + bf, 493 | g = b + bg, 494 | e1=assert(tan_half_angle!=0, "Fillet is not possible on angles of 0 degrees"), 495 | e2=assert(bf_length 1? (sqrt(a) * abs(rx)) : abs(rx), ry = a > 1? (sqrt(a) * abs(ry)) : abs(ry), 545 | co = (long == ccw? 1 : -1) * sqrt(( (rx*rx*ry*ry) - (rx*rx*y*y) - (ry*ry*x*x) ) / ( (rx*rx*y*y) + (ry*ry*x*x) )), 546 | C = ([[ cos(-angle), -sin(-angle)],[sin(-angle), cos(-angle)]] * [rx*y/ry, -ry*x/rx] * co) + ((p1+p2)*0.5)) C; 547 | 548 | 549 | // function pb_ellipseArc(p1, p2, rx, ry, angle, long, ccw) 550 | // 551 | // Produces a list of 2D points that approximates the arc segment required to from p1 to p2. 552 | // p1 2D start point for the arc segment. 553 | // p2 2D end point of the arc segment. 554 | // rx x radius for the ellipse when angle = 0 555 | // ry y radius for the ellipse when angle = 0 556 | // angle rotation angle of the ellipse around it's center point. 557 | // long Two ways around the ellipse. Set to true to take the long way. 558 | // ccw Set to true of you want the acr drawn following the ellipse counter clock wize. 559 | // 560 | // return List with two values. 561 | // return[0] 2D point list forming a polyline representing the ellipseArc. 562 | // return[1] 2D point which represents the position of the ellipse center point. 563 | function pb_ellipseArc(p1=[], p2=[], rx, ry, angle=0, long=false, ccw=false, skip_first=false) = rx==0||ry==0? [p1,p2] : let( 564 | d = norm(p2-p1), 565 | e = assert(rx*2>=d, str("pb_ellipseArc - Radius:",rx," is too small for distance:",d)), 566 | pc = pb_ellipseCenter(p2,p1,rx,ry,angle, long, ccw), 567 | 568 | m = [[cos(angle), -sin(angle)],[sin(angle), cos(angle)]], 569 | nm = [[cos(-angle), -sin(-angle)],[sin(-angle), cos(-angle)]], 570 | v1 = (p1-pc) * nm, v2 = (p2-pc) * nm, 571 | a1 = (v1[1]<0? 180 : 0)+ atan2(v1[0]/v1[1],rx/ry), 572 | a2 = (v2[1]<0? 180 : 0)+ atan2(v2[0]/v2[1],rx/ry), 573 | da = abs(a2 - a1 % 360), das = da<=180? da : 360-da, 574 | cda = long? 360-das : das, 575 | 576 | s = pb_segmentsPerCircle((rx+ry)/2), 577 | 578 | steps = floor(abs(cda*s/360)), 579 | sa = ccw? -(cda/steps) : cda/steps, 580 | pts = steps<=2? [p2] : [for(i=[1:steps-1]) let(a = a1 + (sa * i)%360) pc+[sin(a) * rx , cos(a) * ry] * m, p2] 581 | ) [skip_first? pts : concat([p1],pts),concat(pc,0)]; 582 | 583 | // function pb_curveBetweenPoints(p1, p2, radius) 584 | // 585 | // Creates a curve made of line segments connecting the two points with the defined radius. 586 | // p1 (list) List of two numbers representing a 2D point 587 | // p2 (list) List of two numbers representing a 2D point 588 | // radius (number) Required radius of the desired curve. Change the sign of the radius to mirror the curve. The radius value must be at least half the distance between p1 and p2. 589 | // return (list) List of the resulting curve data... 590 | // return[0] (list) List of the 2D point list describing the curve. The list includes p1 and p2. 591 | // return[1] (number) Angle of the last line segment in the curve 592 | // return[2] (list) List of two numbers representing a 2D point. This point is the center of the circle segment drawn. 593 | function pb_curveBetweenPoints(p1=[], p2=[], radius=0, segments=0) = radius==0? [p1,p2] : let( 594 | d = norm(p2-p1), 595 | r = abs(radius), 596 | e = assert(r*2>=d, str("Radius:",r," is too small for distance:",d)), 597 | x3 = (p1[0] + p2[0])/2, 598 | y3 = (p1[1] + p2[1])/2, 599 | base = sqrt(pow(r,2) - pow((d / 2),2)), 600 | basex = base * (p1[1] - p2[1]) / d, 601 | basey = base * (p2[0] - p1[0]) / d, 602 | pc = radius > 0? [x3 - basex, y3 - basey] : [x3 + basex, y3 + basey], 603 | a1 = atan2(p1[0]-pc[0], p1[1]-pc[1]), 604 | a2 = atan2(p2[0]-pc[0], p2[1]-pc[1]), 605 | da = a2 - a1 % 360, 606 | cda = da<-180? 360 + da : da>180? -360 + da : da, 607 | //steps = floor(abs(cda*($fn==0? 1/$fa : $fn/360))), 608 | steps = segments==0? floor(abs(cda * pb_segmentsPerCircle(radius) / 360)) : segments, 609 | sa = cda/steps, 610 | pts = steps<=2? [p1,p2] : [p1,for(i=[1:steps-1]) [sin(a1 + (sa * i)) * r + pc[0], cos(a1 + (sa * i)) * r + pc[1]],p2] 611 | ) [pts,sign(sa)*90+a1+cda, pc]; 612 | 613 | 614 | 615 | // function pb_bezier_quadratic_curve(p0, c, p1, n) 616 | // 617 | // Generates a Bezier quadratic curve from p0 to p1 using a single control point c. The curve is approximated by a 2D point list containing n points. 618 | // p0 (list) List of two numbers representing the 2D start point of the curve. 619 | // c (list) List of two numbers representing a 2D control point shaping the curve and defines both entry and exit tangents. 620 | // p1 (list) List of two numbers representing the 2D end point of the curve. 621 | // n (number) The number of points that should be returned. 622 | // return (list) List of 2D points resembling the quadratic curve. 623 | function pb_bezier_quadratic_curve(p0, c, p1, n = $pb_spline, skip_first=false) = [for(t = [skip_first? 1: 0 : n]) let(t0=t/n, t1=pow(1 - t0, 2), t2=pow(t0, 2)) [p0[0] * t1 + 2 * c[0] * t0 * (1 - t0) + p1[0] * t2, p0[1] * t1 + 2 * c[1] * t0 * (1 - t0) + p1[1] * t2]]; 624 | 625 | // function pb_bezier_cubic_curve(p0, c0, c1, p1, n) 626 | // 627 | // Generates a Bezier cubic curve from p0 to p1 using two control points c0 and c1. The curve is approximated by a 2D point list containing n points. 628 | // p0 (list) List of two numbers representing the 2D start point of the curve. 629 | // c0 (list) List of two numbers representing a 2D control point shaping the curve and defining the entry tangent. 630 | // c1 (list) List of two numbers representing a 2D control point shaping the curve and defining the exit tangent. 631 | // p1 (list) List of two numbers representing the 2D end point of the curve. 632 | // n (number) The number of points that should be returned. 633 | // return (list) List of 2D points resembling the cubic curve. 634 | function pb_bezier_cubic_curve(p0, c0, c1, p1, n = $pb_spline, skip_first=false) = [for(t = [skip_first? 1: 0 : n]) let(t0=t/n, t1=pow((1 - t0), 3),t2=pow((1 - t0), 2),t3=pow(t0, 2) * (1 - t0), t4=pow(t0, 3)) [ p0[0] * t1 + 3 * c0[0] * t0 * t2 + 3 * c1[0] * t3 + p1[0] * t4, p0[1] * t1 + 3 * c0[1] * t0 * t2 + 3 * c1[1] * t3 + p1[1] * t4]]; 635 | 636 | 637 | //function pb_do_render($children, parent_module_name) = ($children == 0 || parent_module_name == "M" || parent_module_name == "m"); 638 | function pb_do_render(children, parent_module_name) = let( 639 | do_render = (children == 0)// || parent_module_name == "M" || parent_module_name == "m") 640 | ) do_render; 641 | 642 | // module m(x,y,a) 643 | // 644 | // Start and initialise the pathbuilder. The function initialises the global point list $pb_pts, global post processing instructions $pb_post and global angle $pb_angle. 645 | // x (number) x value of the 2D start point initialisation. Default to zero if not provided. 646 | // y (number) y value of the 2D start point initialisation. Default to zero if not provided. 647 | // a (number) angle of the path initialisation. Default to zero if not provided. 648 | module m(x=0, y=0, a=0, $pb_spline=10){ 649 | $pb_fn = $fn; 650 | $pb_fa=$fa; 651 | $pb_fs=$fs; 652 | data = _pb_line([],true, is_list(x)? x: [x,y],a,true); 653 | $pb_pts = data[0]; 654 | $pb_post = data[1]; 655 | $pb_angle = a; 656 | if (pb_do_render($children, parent_module(0))) pb_draw(); 657 | children(); 658 | } 659 | 660 | // module M(x,y,a) 661 | // 662 | // Start and initialise the pathbuilder. The function initialises the global point list $pb_pts, global post processing instructions $pb_post and global angle $pb_angle. 663 | // x (number) x value of the 2D start point initialisation. Default to zero if not provided. 664 | // y (number) y value of the 2D start point initialisation. Default to zero if not provided. 665 | // a (number) angle of the path initialisation. Default to zero if not provided. 666 | module M(x=0, y=0, a=0, $pb_spline=10){ 667 | if (pb_do_render($children, parent_module(0))){ 668 | pb_draw(); 669 | } 670 | children(); 671 | $pb_fn = $fn; 672 | $pb_fa=$fa; 673 | $pb_fs=$fs; 674 | data = _pb_line([],false, is_list(x)? x : [x,y],a,true); 675 | $pb_pts = data[0]; 676 | $pb_post = data[1]; 677 | $pb_angle = a; 678 | } 679 | 680 | // function _pb_horz(last, rel, args, angle) 681 | // 682 | // Adds a point with the same y value as the last point but a new x value. No point is added if the new point is the same as the last point. 683 | // last (list) List of two numbers representing the last known 2D point. 684 | // args (list) Function specific parameters. 685 | // args[0] (number) x value for new 2D point. relative or absolute depending on rel value false or true. 686 | // rel (bool) Set to false when working with absolute coordinates. Set to true when coordinates are relative to the last point. 687 | // angle (number) Last known angle. Returned when no new angle is created. 688 | // return (list) List of a command response alwas consisting of 689 | // return[0] (list) 2D points list of the points generated by the command. The list is empty if not relevant. 690 | // return[1] (list) List representing a post processing command. The list is empty if not relevant. 691 | // return[2] (number) New current angle after the command completed. 692 | function _pb_horz(last=[], args=[], rel=false, angle) = let(x=rel? last[0] + args[0] : args[0]) x==last[0]? [[[]],[],angle,[[],[]]] : [[[x, last[1]]], [], x > last[0]? 90 : 270,[[],[]]]; 693 | 694 | // module h(last, rel, args) 695 | // 696 | // Adds a point with the same y value as the last point but a new x value relative from the last known point. 697 | // x (number) x value for new 2D point relative from the last known 2D point. 698 | module h(x){ 699 | data = _pb_horz(pb_last($pb_pts), [x], true, $pb_angle); 700 | $pb_pts = concat($pb_pts, data[0]); 701 | $pb_angle = data[2]==undef? $pb_angle : data[2]; 702 | $pb_ctrl_pts = data[3]; 703 | if (pb_do_render($children, parent_module(0))) pb_draw(); 704 | children(); 705 | } 706 | 707 | // module H(last, rel, args) 708 | // 709 | // Adds a point with the same y value as the last point but a new absolute x value . 710 | // x (number) x value for new 2D point in absolute coordinates. 711 | module H(x){ 712 | data = _pb_horz(pb_last($pb_pts), [x], false, $pb_angle); 713 | $pb_pts = concat($pb_pts, data[0]); 714 | $pb_angle = data[2]; 715 | $pb_ctrl_pts = data[3]; 716 | if (pb_do_render($children, parent_module(0))) pb_draw(); 717 | children(); 718 | } 719 | 720 | // function _pb_vert(last, rel, args, angle) 721 | // 722 | // Adds a point vertically from the last point y units away. 723 | // pts (list) The point list being constructed. 724 | // args (list) Function specific parameters. 725 | // args[0] (number) Distance along y axis either relative or absolute 726 | // rel (bool) Set to false when working with relative values. Set to true when working with absolute values. 727 | // angle (number) Last known angle. Returned when no new angle is created. 728 | // return (list) List of a command response alwas consisting of 729 | // return[0] (list) 2D points list of the points generated by the command. The list is empty if not relevant. 730 | // return[1] (list) List representing a post processing command. The list is empty if not relevant. 731 | // return[2] (number) New current angle after the command completed. 732 | function _pb_vert(last=[], args=[], rel=false, angle) = let(y=rel? last[1] + args[0] : args[0]) y==last[1]? [[[]],[],angle,[[],[]]] : [[[last[0],y]], [], y > last[0]? 0 : 180,[[],[]]]; 733 | 734 | // Adds point y units away from the last 735 | module v(y){ 736 | data = _pb_vert(pb_last($pb_pts), [y], true, $pb_angle); 737 | $pb_pts = concat($pb_pts, data[0]); 738 | $pb_angle = data[2]; 739 | $pb_ctrl_pts = data[3]; 740 | if (pb_do_render($children, parent_module(0))) pb_draw(); 741 | children(); 742 | } 743 | 744 | // Adds a point vertically from the last point to absolute coordinate y 745 | module V(y){ 746 | data = _pb_vert(pb_last($pb_pts), [y], false, $pb_angle); 747 | $pb_pts = concat($pb_pts, data[0]); 748 | $pb_angle = data[2]; 749 | $pb_ctrl_pts = data[3]; 750 | if (pb_do_render($children, parent_module(0))) pb_draw(); 751 | children(); 752 | } 753 | 754 | // function _pb_close(pts, angle) 755 | // 756 | // Closes points list if not already closed by adding the first point to the end of the list. 757 | // pts (list) The point list being constructed. 758 | // angle (number) Last known angle. Returned when no new angle is created. 759 | // return (list) List of a command response alwas consisting of 760 | // return[0] (list) 2D points list of the points generated by the command. The list is empty if not relevant. 761 | // return[1] (list) List representing a post processing command. The list is empty if not relevant. 762 | // return[2] (number) New current angle after the command completed. 763 | function _pb_close(pts, angle) = [[], [], angle]; 764 | 765 | // function _pb_line 766 | // 767 | // Adds one or multiple points starting from last point x units away. Points are only added if the previous point is different. 768 | // pts (list) List of points created so far. 769 | // rel (bool) Set to false when working with relative values. Set to true when working with absolute values. 770 | // args (list) Flat list of numbers. Numbers are x,y value pairs. Should have 1 .. n value pairs. 771 | // args[0] (number) x value for the next point. 772 | // args[1] (number) y value for the next point. 773 | // args[...] (number) provide as many x,y value pairs as required like this U shape [0,5, 0,0, 5,0, 5,5] 774 | // angle (number) Last known angle. Returned when no new angle is created. 775 | // return (list) List of a command response alwas consisting of 776 | // return[0] (list) 2D points list of the points generated by the command. The list is empty if not relevant. 777 | // return[1] (list) List representing a post processing command. The list is empty if not relevant. 778 | // return[2] (number) New current angle after the command completed. 779 | function _pb_line(pts=[], rel=false, args=[], angle, move=false, _i=0, _g, _r=[]) = let( 780 | _l = _r==[]? pts==[]? [0,0] : pb_last(pts) : pb_last(_r), 781 | _g = _g==undef? pb_groupsOf(2,args)[1] : _g, 782 | np = rel? _l+_g[_i] : _g[_i], 783 | _r = np==_l&&pts!=[]&&_r!=[]? _r : concat(_r, [np]) 784 | ) _i>=len(_g)-1? [_r,move?[[0,0,0]]:[],move? angle: pb_calcExitAngle(concat([_l],_r),angle),[[],[]]] : _pb_line(pts, rel, args, angle, move, _i+1, _g, _r); 785 | 786 | // module l 787 | // 788 | // Adds one or multiple points relative from the last point 789 | // x (list) Flat list of numbers. Numbers are x,y value pairs. Should have 1 .. n value pairs or... 790 | // x (number) x value for the next point. 791 | // y (number) y value for the next point. 792 | module l(x, y){ 793 | args = is_list(x)? x : [x,y]; 794 | data = _pb_line($pb_pts, true, args, $pb_angle, false); 795 | $pb_pts = concat($pb_pts, data[0]); 796 | $pb_angle = data[2]; 797 | $pb_ctrl_pts = data[3]; 798 | if (pb_do_render($children, parent_module(0))) pb_draw(); 799 | children(); 800 | } 801 | 802 | // module L 803 | // 804 | // Adds one or multiple points. Values are absolute coordinates. 805 | // x (list) Flat list of numbers. Numbers are x,y value pairs. Should have 1 .. n value pairs or... 806 | // x (number) x value for the next point. 807 | // y (number) y value for the next point. 808 | module L(x, y){ 809 | args = is_list(x)? x : [x,y]; 810 | data = _pb_line($pb_pts, false, args, $pb_angle, false); 811 | $pb_pts = concat($pb_pts, data[0]); 812 | $pb_angle = data[2]; 813 | $pb_ctrl_pts = data[3]; 814 | if (pb_do_render($children, parent_module(0))) pb_draw(); 815 | children(); 816 | } 817 | 818 | // function _pb_cubic(last, args, rel, angle) 819 | // 820 | // Generates multiple points forming line segments approximating one or multiple discrete cubic Bezier curves. 821 | // 822 | function _pb_cubic(last=[], args=[], rel=false, angle, _i=0, _g, _r=[]) = let( 823 | _g = _g==undef? pb_groupsOf(6, args)[1] : _g, 824 | b = _g[_i], 825 | p0 = last, 826 | c0 = rel? last + [b[0],b[1]] : [b[0],b[1]], 827 | c1 = rel? last + [b[2],b[3]] : [b[2],b[3]], 828 | p1 = rel? last + [b[4],b[5]] : [b[4],b[5]], 829 | _r = concat(_r, pb_bezier_cubic_curve(p0, c0, c1, p1, skip_first=true))) _i==len(_g)-1? [_r,[],pb_calcExitAngle(_r),[[],pb_reflectPntOn(c1, p1)]] : _pb_cubic(p1, args, rel, angle, _i+1, _g, _r); 830 | 831 | module c(cx1, cy1, cx2, cy2, x, y){ 832 | args = is_num(cx1)? [cx1, cy1, cx2, cy2, x, y] : cx1; 833 | data = _pb_cubic(pb_last($pb_pts), args, true, $pb_angle); 834 | $pb_pts = concat($pb_pts, data[0]); 835 | $pb_angle = data[2]; 836 | $pb_ctrl_pts = data[3]; 837 | if (pb_do_render($children, parent_module(0))) pb_draw(); 838 | children(); 839 | } 840 | 841 | module C(cx1, cy1, cx2, cy2, x, y){ 842 | args = is_num(cx1)? [cx1, cy1, cx2, cy2, x, y] : cx1; 843 | data = _pb_cubic(pb_last($pb_pts), args, false, $pb_angle); 844 | $pb_pts = concat($pb_pts, data[0]); 845 | $pb_angle = data[2]; 846 | $pb_ctrl_pts = data[3]; 847 | if (pb_do_render($children, parent_module(0))) pb_draw(); 848 | children(); 849 | } 850 | 851 | // function _pb_smooth_cubic(last, args, rel, angle) 852 | // 853 | // Generates multiple points forming line segments approximating one or multiple discrete cubic Bezier curves. 854 | // 855 | function _pb_smooth_cubic(last=[], args=[], rel=false, angle, ctrl_pts, _i=0, _g, _r=[]) = let( 856 | _g = _g==undef? pb_groupsOf(4, args)[1] : _g, 857 | b = _g[_i], 858 | p0 = last, 859 | c0 = len(ctrl_pts[1])==2? ctrl_pts[1] : p0, 860 | c1 = rel? last + [b[0],b[1]] : [b[0],b[1]], 861 | p1 = rel? last + [b[2],b[3]] : [b[2],b[3]], 862 | cn = pb_reflectPntOn(c1,p1), 863 | _r = concat(_r, pb_bezier_cubic_curve(p0, c0, c1, p1, skip_first=true))) _i==len(_g)-1? [_r,[], pb_calcExitAngle(_r),[[],cn]] : _pb_smooth_cubic(p1, args, rel, angle, [[],cn], _i+1, _g, _r); 864 | 865 | module s(cx2, cy2, x, y, n=$pb_spline){ 866 | args = is_num(cx2)? [cx2, cy2, x, y] : cx2; 867 | data = _pb_smooth_cubic(pb_last($pb_pts), args, true, $pb_angle, $pb_ctrl_pts,$pb_spline); 868 | $pb_pts = concat($pb_pts, data[0]); 869 | $pb_angle = data[2]; 870 | $pb_ctrl_pts = data[3]; 871 | if (pb_do_render($children, parent_module(0))) pb_draw(); 872 | children(); 873 | } 874 | 875 | module S(cx2, cy2, x, y, n=$pb_spline){ 876 | args = is_num(cx2)? [cx2, cy2, x, y] : cx2; 877 | data = _pb_smooth_cubic(pb_last($pb_pts), args, false, $pb_angle, $pb_ctrl_pts,$pb_spline); 878 | $pb_pts = concat($pb_pts, data[0]); 879 | $pb_angle = data[2]; 880 | $pb_ctrl_pts = data[3]; 881 | if (pb_do_render($children, parent_module(0))) pb_draw(); 882 | children(); 883 | } 884 | 885 | // function _pb_quadratic(last, args, rel, angle,ctrl_pt) 886 | // 887 | // Generates multiple points forming line segments approximating one or multiple discrete cubic Bezier curves. 888 | // 889 | function _pb_quadratic(last=[], args=[], rel=false, angle, _i=0, _g, _r=[]) = let( 890 | _g = _g==undef? pb_groupsOf(4, args)[1] : _g, 891 | b = _g[_i], 892 | p0 = last, 893 | c = rel? last + [b[0],b[1]] : [b[0],b[1]], 894 | p1 = rel? last + [b[2],b[3]] : [b[2],b[3]], 895 | _r = concat(_r, pb_bezier_quadratic_curve(p0, c, p1, skip_first=true))) _i==len(_g)-1? [_r,[],pb_calcExitAngle(_r),[pb_reflectPntOn(c,p1),[]]] : _pb_quadratic(p1, args, rel, angle, _i+1, _g, _r); 896 | 897 | module q(cx, cy, x, y, n=$pb_spline){ 898 | args = is_num(cx)? [cx, cy, x, y] : cx; 899 | data = _pb_quadratic(pb_last($pb_pts), args, true, $pb_angle); 900 | $pb_pts = concat($pb_pts, data[0]); 901 | $pb_angle = data[2]; 902 | $pb_ctrl_pts = data[3]; 903 | if (pb_do_render($children, parent_module(0))) pb_draw(); 904 | children(); 905 | } 906 | 907 | module Q(cx, cy, x, y, n=$pb_spline){ 908 | args = is_num(cx)? [cx, cy, x, y] : cx; 909 | data = _pb_quadratic(pb_last($pb_pts), args, false, $pb_angle); 910 | $pb_pts = concat($pb_pts, data[0]); 911 | $pb_angle = data[2]; 912 | $pb_ctrl_pts = data[3]; 913 | if (pb_do_render($children, parent_module(0))) pb_draw(); 914 | children(); 915 | } 916 | 917 | // function _pb_smooth_quadratic(last, args, rel, angle) 918 | // 919 | // Generates multiple points forming line segments approximating one or multiple discrete cubic Bezier curves. 920 | // 921 | function _pb_smooth_quadratic(last=[], args=[], rel=false, angle, ctrl_pts, _i=0, _g, _r=[]) = let( 922 | _g = _g==undef? pb_groupsOf(2, args)[1] : _g, 923 | b = _g[_i], 924 | p0 = last, 925 | c = len(ctrl_pts[0])==2? ctrl_pts[0] : p0, 926 | p1 = rel? last + [b[0],b[1]] : [b[0],b[1]], 927 | cn = pb_reflectPntOn(c, p1), 928 | _r = concat(_r, pb_bezier_quadratic_curve(p0, c, p1, skip_first=true))) _i==len(_g)-1? [_r,[],pb_calcExitAngle(_r),[cn,[]]] : _pb_smooth_quadratic(p1, args, rel, angle, [cn,[]], _i+1, _g, _r); 929 | 930 | module t(x, y, n=$pb_spline){ 931 | args = is_num(x)? [x, y] : x; 932 | data = _pb_smooth_quadratic(pb_last($pb_pts), args, true, $pb_angle, $pb_ctrl_pts); 933 | $pb_pts = concat($pb_pts, data[0]); 934 | $pb_angle = data[2]; 935 | $pb_ctrl_pts = data[3]; 936 | if (pb_do_render($children, parent_module(0))) pb_draw(); 937 | children(); 938 | } 939 | 940 | module T(x, y, n=$pb_spline){ 941 | args = is_num(x)? [x, y] : x; 942 | data = _pb_smooth_quadratic(pb_last($pb_pts), args, false, $pb_angle, $pb_ctrl_pts); 943 | $pb_pts = concat($pb_pts, data[0]); 944 | $pb_angle = data[2]; 945 | $pb_ctrl_pts = data[3]; 946 | if (pb_do_render($children, parent_module(0))) pb_draw(); 947 | children(); 948 | } 949 | 950 | // function _pb_arc(last, args, rel, angle) 951 | // 952 | // Generates multiple points forming line segments approximating one or multiple discrete cubic Bezier curves. 953 | // 954 | function _pb_arc(last=[], args=[], rel=false, angle, _i=0, _g, _r=[]) = let( 955 | _g = _g==undef? pb_groupsOf(7, args)[1] : _g, 956 | b = _g[_i], 957 | rx = b[0], 958 | ry = b[1], 959 | angle = b[2], 960 | long = b[3], 961 | sweep = b[4], 962 | p2 = rel? last + [b[5], b[6]] : [b[5], b[6]], 963 | d = pb_ellipseArc(last, p2, rx, ry, angle, long, sweep, true), 964 | _r = concat(_r, d[0])) _i==len(_g)-1? [_r, [], pb_calcExitAngle(d[0]),[[],[]]] : _pb_arc(p2, args, rel, angle, _i+1, _g, _r); 965 | 966 | // arc creates an ellipse according the x and y radius. 967 | // 968 | // rx (number) Radius for the ellipse along the x axis. 969 | // ry (number) Radius for the ellipse along the y axis. 970 | // angle (number) Degrees of rotation for the ellipse. 971 | // long (bool) Ensures arc will be greater than 180 degrees when true. 972 | // ccw (bool) Ensures arc will be drawn counter clockwize when true. 973 | // x (number) x value of the desired 2D end point. 974 | // y (number) y value of the desired 2D end point. 975 | 976 | module a(rx, ry, angle, long, sweep, x, y){ 977 | args = is_num(rx)? [rx, ry, angle, long, sweep, x, y] : rx; 978 | data = _pb_arc(pb_last($pb_pts), args, true, 0, 0, undef, []); 979 | $pb_pts = concat($pb_pts, data[0]); 980 | $pb_angle = data[2]; 981 | $pb_ctrl_pts = data[3]; 982 | if (pb_do_render($children, parent_module(0))) pb_draw(); 983 | children(); 984 | } 985 | 986 | module A(rx, ry, angle, long, sweep, x, y){ 987 | args = is_num(rx)? [rx, ry, angle, long, sweep, x, y] : rx; 988 | data = _pb_arc(pb_last($pb_pts), args, false, 0, 0, undef, []); 989 | $pb_pts = concat($pb_pts, data[0]); 990 | $pb_angle = data[2]; 991 | $pb_ctrl_pts = data[3]; 992 | if (pb_do_render($children, parent_module(0))) pb_draw(); 993 | children(); 994 | } 995 | 996 | // function _pb_polar(pts, args) 997 | // 998 | // Adds a point at distance d relative from the last point in the given direction 999 | // relative of absolute angle is handled by the caller. Here we assume absolute angle 1000 | // pts the point list being constructed 1001 | // args (list) Function specific parameters 1002 | // args[0] (number) Distance along from the last point. 1003 | // args[1] (number) Angle along which the new point is calculated. 1004 | // return (list) Data structure being [new_points_list, new_post_processing_instructions_list, new_angle]. 1005 | function _pb_polar(pts=[], args=[]) = let(l= pb_last(pts)) 1006 | [len(args)<2? [] : [l+[sin(args[1])*args[0], cos(args[1])*args[0]]],[],args[1],[[],[]]]; 1007 | 1008 | module polar(d, a){ 1009 | data = _pb_polar($pb_pts, [d,$pb_angle+a]); 1010 | $pb_pts = concat($pb_pts, data[0]); 1011 | $pb_angle = data[2]; 1012 | $pb_ctrl_pts = data[3]; 1013 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1014 | children(); 1015 | } 1016 | 1017 | module Polar(d, a){ 1018 | data = _pb_polar($pb_pts,[d,a]); 1019 | $pb_pts = concat($pb_pts, data[0]); 1020 | $pb_angle = data[2]; 1021 | $pb_ctrl_pts = data[3]; 1022 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1023 | children(); 1024 | } 1025 | 1026 | // function _pb_forward(last, args, rel, angle) 1027 | // 1028 | // Adds a point some distance along in the same direction as the last line segment or last angle. 1029 | // The distance depends on the arguments provided. Extends last line to intersect with one or multiple reference lines described by the provided parameters. 1030 | // The function will not add points if the last line is parallel with the reference line(s) or when it doesn't satisfy the bounds_mode rules. 1031 | // last (list) The point list being constructed. 1032 | // rel (bool) Set to false when working with relative boundary values. Set to true when working with absolute boundary values. 1033 | // args (list) Function specific parameters. 1034 | // args[0] If a single value is provided the last line segment is extended with that value. 1035 | // angle (number) Last known angle. 1036 | // If two values are provided the last line is extended to wich ever line is intersected with first either the line with the value x or reference lines are assumed to lie parallel with both x and y axis. Two values provide a reference line points are given the intersection with that line. 1037 | function _pb_forward(last=[0,0], args=[], rel=false, angle) = let( 1038 | l = len(args), p1 = last, d = l==1? args[0] : 0.001, p2 = p1+[sin(angle)*d, cos(angle)*d], 1039 | bds = [for(b=l==1? [] : l==2? [[args[0]-1,args[1]],[args[0],args[1]],[args[0],args[1]+1]] : pb_groupsOf(2, args)[1]) rel? last+b : b], 1040 | pt = l==1? [[p2]] : l>1? pb_intersectLineWithPolyline([p1,p2],bds,true,true, false, l==2?undef:true) : [[[]]]) [len(pt)>0?[pt[0][0]]:[],[],angle,[[],[]]]; 1041 | 1042 | module forward(d){ 1043 | data = _pb_forward(pb_last($pb_pts), is_num(d)? [d] : d, true, $pb_angle); 1044 | $pb_pts = concat($pb_pts, data[0]); 1045 | $pb_ctrl_pts = data[3]; 1046 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1047 | children(); 1048 | } 1049 | 1050 | module Forward(d){ 1051 | data = _pb_forward(pb_last($pb_pts), is_num(d)? [d] : d, false, $pb_angle); 1052 | $pb_pts = concat($pb_pts, data[0]); 1053 | $pb_ctrl_pts = data[3]; 1054 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1055 | children(); 1056 | } 1057 | 1058 | // Generates multiple points to form a circle segment from the last point to relative point x,y with given radius 1059 | // An error is shown if the radius is less than half the distance between the two points. 1060 | // You can flip the circle segment inside out by changing the radius to negative 1061 | function _pb_segment(last=[], args=[], rel=false, _i=0, _r=[]) = let( 1062 | groups = pb_groupsOf(2,args,0,1,true), 1063 | r = groups[2][0], 1064 | $fn = len(groups[2])>1? groups[2][1] : $fn, 1065 | pt2 = rel? last+groups[1][_i] : groups[1][_i], 1066 | data = pb_curveBetweenPoints(last, pt2, r), 1067 | _r = concat(_r, pb_subList(data[0], 1)) 1068 | ) _i==len(groups[1])-1? [_r,[],data[1],[[],[]]] : _pb_segment(last=pt2, args=args, rel, _i=_i+1, _r=_r); 1069 | 1070 | module segment(x, y, r){ 1071 | l = pb_last($pb_pts); 1072 | args = is_num(x)? [x,y,r] : is_num(r)? concat(x, r) : x; 1073 | data = _pb_segment(l, args, true); 1074 | $pb_pts = concat($pb_pts,data[0]); 1075 | $pb_angle = data[2]; 1076 | $pb_ctrl_pts = data[3]; 1077 | $fn = $pb_fn; $fa = $pb_fa; $fs = $pb_fs; 1078 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1079 | children(); 1080 | } 1081 | 1082 | // Adds multiple points to form a circle segment from the last point to absolute point x,y with given radius 1083 | // An error is shown if the radius is less than half the distance between the two points. 1084 | // You can flip the circle segment inside out by changing the radius to negative 1085 | module Segment(x, y, r){ 1086 | args = is_num(x)? [x,y,r] : is_num(r)? concat(x, r) : x; 1087 | data = _pb_segment(pb_last($pb_pts), args, false); 1088 | $pb_pts = concat($pb_pts,data[0]); 1089 | $pb_angle = data[2]; 1090 | $pb_ctrl_pts = data[3]; 1091 | $fn = $pb_fn; $fa = $pb_fa; $fs = $pb_fs; 1092 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1093 | children(); 1094 | } 1095 | 1096 | function _pb_angle(args, rel=false, angle) = [[],[], rel? angle + args[0] : args[0],[[],[]]]; 1097 | 1098 | module angle(a){ 1099 | data =_pb_angle([a], true, $pb_angle); 1100 | $pb_angle = data[2]; 1101 | $pb_ctrl_pts = data[3]; 1102 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1103 | children(); 1104 | } 1105 | 1106 | module Angle(a){ 1107 | data = _pb_angle([a],false, $pb_angle); 1108 | $pb_angle = data[2]; 1109 | $pb_ctrl_pts = data[3]; 1110 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1111 | children(); 1112 | } 1113 | 1114 | // Inserts a fillet at the current point with the given radius and optional fixed number of segments. set radius to negative to turn the fillet outwards 1115 | function _pb_fillet(pts, args=[], angle) = [[],args[0]==0? [] : [[2, len(pts)-1, args[0], args[1]? args[1] : $fn]],angle]; 1116 | module fillet(r, s){ 1117 | data = _pb_fillet($pb_pts, [r, s], $pb_angle); 1118 | $pb_post = concat($pb_post, data[1]); // fillet tag 1119 | $pb_angle = data[2]; 1120 | $pb_ctrl_pts = data[3]; 1121 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1122 | children(); 1123 | } 1124 | 1125 | // Inserts a chamfer with size s on this point 1126 | function _pb_chamfer(pts, args=[], angle) = [[],args[0]==0? [] : [[3,len(pts)-1, args[0]]],angle,[[],[]]]; 1127 | module chamfer(s){ 1128 | data = _pb_chamfer($pb_pts, [s],$pb_angle); 1129 | $pb_post = concat($pb_post, data[1]); // chamfer tag 1130 | $pb_angle = data[2]; 1131 | $pb_ctrl_pts = data[3]; 1132 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1133 | children(); 1134 | } 1135 | 1136 | // Draws the final shape of $pb_pts as a polygon 1137 | module pb_draw(){ 1138 | data1 = [$pb_pts, concat($pb_post, [[4,len($pb_pts)-1]])]; 1139 | points1 = pb_postProcessPathLists([data1]); 1140 | polygon(points1[0]); 1141 | //if (pb_do_render($children, parent_module(0))) pb_draw(); 1142 | children(); 1143 | } 1144 | 1145 | module print(index = 0){ 1146 | data1 = [$pb_pts, concat($pb_post, [[4,len($pb_pts)-1]])]; 1147 | points1 = pb_postProcessPathLists([data1]); 1148 | echo("**********************************"); 1149 | echo(str("print Points: ", points1[index])); 1150 | echo("**********************************"); 1151 | if (pb_do_render($children, parent_module(0))) pb_draw(); 1152 | children(); 1153 | } 1154 | 1155 | // Draws the final shape of $pb_pts as a polygon 1156 | function points() = let( 1157 | data1 = [$pb_pts, concat($pb_post, [[4,len($pb_pts)-1]])], 1158 | points1 = pb_postProcessPathLists([data1]) 1159 | ) points1[0]; 1160 | --------------------------------------------------------------------------------