跳转到内容

OpenSCAD 用户手册/技巧与窍门

来自 Wikibooks,开放世界的开放书籍

许可证说明

[编辑 | 编辑源代码]

此页面上显示的所有代码片段都旨在免费使用,无需任何署名,也不限于任何用途,例如,将此处的所有代码贡献视为放置在公共领域或 CC0 许可证下。 这并不意味着要更改页面的整体正常许可证和/或手册本身。

从列表映射值

[编辑 | 编辑源代码]
// The function that maps input values x to output values, the
// example uses floor() to convert floating point to integer
// values.
function map(x) = floor(x);
  
input = [58.9339, 22.9263, 19.2073, 17.8002, 40.4922, 19.7331, 38.9541, 28.9327, 18.2059, 75.5965];
  
// Use a list comprehension expression to call the map() function
// for every value of the input list and put the result of the
// function in the output list.
output = [ for (x = input) map(x) ];
  
echo(output);
// ECHO: [58, 22, 19, 17, 40, 19, 38, 28, 18, 75]

过滤列表中的值

[编辑 | 编辑源代码]
// The function that define if the input value x should be
// included in the filtered list, the example selects
// all even values that are greater than 6.
function condition(x) = (x >= 6) && (x % 2 == 0);
  
input = [3, 3.3, 4, 4.1, 4.8, 5, 6, 6.3, 7, 8];
  
// Use a list comprehension expression to call the condition()
// function for every value of the input list and put the value
// in the output list if the function returns true.
output = [ for (x = input) if (condition(x)) x ];
  
echo(output);
// ECHO: [6, 8]

添加列表中的所有值

[编辑 | 编辑源代码]
// Create a simple recursive function that adds the values of a list of floats;
// the simple tail recursive structure makes it possible to
// internally handle the calculation as loop, preventing a
// stack overflow.
function add(v, i = 0, r = 0) = i < len(v) ? add(v, i + 1, r + v[i]) : r;
 
input = [2, 3, 5, 8, 10, 12];
 
output = add(input);

echo(output);
// ECHO: 40
//------------------ add2 -----------------------
// An even simpler non recursive code version of add explores the 
// the matrix product operator
function add2(v) = [for(p=v) 1]*v;

echo(add2(input));
// ECHO: 40

// add2 works also with lists of vectors
input2 = [ [2, 3] , [5, 8] , [10, 12] ];
echo(add2(input2));
// ECHO: [17, 23]
echo(add(input2));
// ECHO: undef  // Why?
//----------------- add3 --------------------------
// With a little more code, the function add may be used also 
// to add any homogeneous list structure of floats
function add3(v, i = 0, r) = 
    i < len(v) ? 
        i == 0 ?
            add3(v, 1, v[0]) :
            add3(v, i + 1, r + v[i]) :
        r;

input3 = [ [[1], 1] , [[1], 2] , [[1], 3] ];
input4 = [ 10, [[1], 1] , [[1], 2] , [[1], 3] ];

echo(add3(input3));
// ECHO: [[3], 6]
echo(add2(input3));
// ECHO: undef // input3 is not a list of vectors
echo(add3(input4));
// ECHO: undef // input4 is not a homogeneous list

累积和

[编辑 | 编辑源代码]

[注意: 需要版本2019.05]

//create a cumulative-sum function using a c-style generator
values = [1,2,65,1,4];

cumsum = [ for (a=0, b=values[0]; a < len(values); a= a+1, b=b+values[a]) b];

// Does not cause a warning "WARNING: undefined operation (number + undefined) in file ..."
cumsum2 = [ for (a=0, b=values[0]; a < len(values); a= a+1, b=b+(values[a]==undef?0:values[a])) b];

echo(cumsum);
// ECHO: [1, 3, 68, 69, 73]
echo(cumsum2);
// ECHO: [1, 3, 68, 69, 73]

计算列表中满足条件的值的数量

[编辑 | 编辑源代码]
// The function that define if the input value x should be
// included in the filtered list, the example selects
// all even values that are greater than 6.
function condition(x) = (x >= 6) && (x % 2 == 0);
 
input = [3, 3.3, 4, 4.1, 4.8, 5, 6, 6.3, 7, 8];
 
// Use a list comprehension expression to call the condition()
// function for every value of the input list and put the value
// in the output list if the function returns true.
// Finally the count is determined simply by using len() on the
// filtered list.
output = len([ for (x = input) if (condition(x)) x ]);
 
echo(output);
// ECHO: 2

查找列表中最大值的索引

[编辑 | 编辑源代码]
// Create a function that find the index of the maximum value
// found in the input list of floats
function index_max(l) = search(max(l), l)[0];

input = [ 6.3, 4, 4.1, 8, 7, 3, 3.3, 4.8, 5, 6];

echo(index_max(input));
// Check it
echo(input[index_max(input)] == max(input));
// ECHO: 3
// ECHO: true

关注 undef

[编辑 | 编辑源代码]

OpenSCAD 中的大多数非法操作都会返回undef。有些返回nan。但是,程序会继续运行,如果未采取预防措施,undef值可能会导致不可预测的未来行为。当函数调用中缺少函数参数时,在计算函数表达式时会将其分配给undef值。为了避免这种情况,可以为可选函数参数分配默认值。

// add 'a' to each element of list 'L'
function incrementBy(L, a) =  [ for(x=L) x+a ];

//add 'a' to each element of list 'L'; 'a' default is 1 when missing
function incrementByWithDefault(L, a=1) = [ for(x=L) x+a ];

echo(incrementBy= incrementBy([1,2,3],2));
echo(incrementByWithDefault= incrementByWithDefault([1,2,3],2));
echo(incrementBy= incrementBy([1,2,3]));
echo(incrementByWithDefault= incrementByWithDefault([1,2,3]));
// ECHO: incrementBy= [3, 4, 5]
// ECHO: incrementByWithDefault= [3, 4, 5]
// ECHO: incrementBy= [undef, undef, undef]
// ECHO: incrementByWithDefault= [2, 3, 4]

有时默认值取决于调用的其他参数,不能像以前那样设置;条件表达式可以解决此问题

// find the sublist of 'list' with indices from 'from' to 'to' 
function sublist(list, from=0, to) =
    let( end = (to==undef ? len(list)-1 : to) )
    [ for(i=[from:end]) list[i] ];

echo(s0= sublist(["a", "b", "c", "d"]) );  	// from = 0, end = 3
echo(s1= sublist(["a", "b", "c", "d"], 1, 2) ); // from = 1, end = 2
echo(s2= sublist(["a", "b", "c", "d"], 1)); 	// from = 1, end = 3
echo(s3= sublist(["a", "b", "c", "d"], to=2) );	// from = 0, end = 2
// ECHO: s0 = ["a", "b", "c", "d"]
// ECHO: s1 = ["b", "c"] 
// ECHO: s2 = ["b", "c", "d"] 
// ECHO: s3 = ["a", "b", "c"]

from > to时,函数sublist()会返回不希望的值并生成警告(试试看!)。在这种情况下,一个简单的解决方案是返回空列表[]

// returns an empty list when 'from > to' 
function sublist2(list, from=0, to) =
    from<=to ?
    	let( end = (to==undef ? len(list)-1 : to) )
    	[ for(i=[from:end]) list[i] ] :
	[];

echo(s1= sublist2(["a", "b", "c", "d"], 3, 1));
echo(s2= sublist2(["a", "b", "c", "d"], 1));
echo(s3= sublist2(["a", "b", "c", "d"], to=2));
// ECHO: s1 = []
// ECHO: s2 = []
// ECHO: s3 = ["a", "b", "c"]

上面的输出s2是空列表,因为to==undef并且fromto的比较结果为falseto的默认值已丢失。要克服这一点,只需反转测试即可

function sublist3(list, from=0, to) =
    from>to ?
	[] :
    	let( end = to==undef ? len(list)-1 : to )
    	[ for(i=[from:end]) list[i] ] ;

echo(s1=sublist3(["a", "b", "c", "d"], 3, 1));
echo(s2=sublist3(["a", "b", "c", "d"], 1));
echo(s3=sublist3(["a", "b", "c", "d"], to=2));
// ECHO: s1 = []
// ECHO: s2 = ["b", "c", "d"]
// ECHO: s3 = ["a", "b", "c"]

现在,当to未定义时,第一个测试结果为假,并且执行let()。通过仔细选择测试,我们可以处理undef值。

将圆柱体堆叠在一起

[编辑 | 编辑源代码]
OpenSCAD - 堆叠圆柱体
// Define the sizes for the cylinders, first value is the
// radius, the second is the height.
// All cylinders are to be stacked above each other (with
// an additional spacing of 1 unit).
sizes = [ [ 26, 3 ], [ 20, 5 ], [ 11, 8 ],  [ 5, 10 ], [ 2, 13 ] ];

// One option to solve this is by using a recursive module
// that creates a new translated coordinate system before
// going into the next level.
module translated_cylinder(size_vector, idx = 0) {
    if (idx < len(size_vector)) {
        radius = size_vector[idx][0];
        height = size_vector[idx][1];

        // Create the cylinder for the current level.
        cylinder(r = radius, h = height);

        // Recursive call generating the next cylinders
        // translated in Z direction based on the height
        // of the current cylinder
        translate([0, 0, height + 1]) {
            translated_cylinder(size_vector, idx + 1);
        }
    }
}

// Call the module to create the stacked cylinders.
translated_cylinder(sizes);

最小旋转问题

[编辑 | 编辑源代码]

在二维空间中,除了非常特殊的情况外,只有两种旋转可以使向量与另一个向量对齐。在三维空间中,有无限多种。然而,只有一种具有最小的旋转角度。以下函数构建了该最小旋转的矩阵。该代码是对在 Oskar Linde 的 sweep.scad 中找到的函数的简化。

// Find the unitary vector with direction v. Fails if v=[0,0,0].
function unit(v) = norm(v)>0 ? v/norm(v) : undef; 
// Find the transpose of a rectangular matrix
function transpose(m) = // m is any rectangular matrix of objects
  [ for(j=[0:len(m[0])-1]) [ for(i=[0:len(m)-1]) m[i][j] ] ];
// The identity matrix with dimension n
function identity(n) = [for(i=[0:n-1]) [for(j=[0:n-1]) i==j ? 1 : 0] ];

// computes the rotation with minimum angle that brings a to b
// the code fails if a and b are opposed to each other
function rotate_from_to(a,b) = 
    let( axis = unit(cross(a,b)) )
    axis*axis >= 0.99 ? 
        transpose([unit(b), axis, cross(axis, unit(b))]) * 
            [unit(a), axis, cross(axis, unit(a))] : 
        identity(3);

在 OpenSCAD 中绘制“线”

[编辑 | 编辑源代码]
OpenSCAD - 结
// An application of the minimum rotation
// Given two points p0 and p1, draw a thin cylinder with its
// bases at p0 and p1
module line(p0, p1, diameter=1) {
    v = p1-p0;
    translate(p0)
        // rotate the cylinder so its z axis is brought to direction v
        multmatrix(rotate_from_to([0,0,1],v))
            cylinder(d=diameter, h=norm(v), $fn=4);
}
// Generate the polygonal points for the knot path 
knot = [ for(i=[0:2:360])
         [ (19*cos(3*i) + 40)*cos(2*i),
           (19*cos(3*i) + 40)*sin(2*i),
            19*sin(3*i) ] ];
// Draw the polygonal a segment at a time
for(i=[1:len(knot)-1]) 
    line(knot[i-1], knot[i], diameter=5);
// Line drawings with this function is usually excruciatingly lengthy to render
// Use it just in preview mode to debug geometry

旋转规则帮助中可以找到另一种 line() 模块的方法。

凸包序列或链

[编辑 | 编辑源代码]

使用循环,即使使用少量参数,也可以通过迭代生成船体片段来组成复杂的模型。以下示例中的循环体被重复评估,使用'i'的值,从i=1开始,每次增加1,直到评估到i=18并在评估到i=19之前终止。这为'i'和'j'的值生成了以下二维数组:[[1,2], [2,3], [3,4], ..., [16,17], [17,18], [18, 19]]

for (i=[1:18]){
  j=i+1;
  hull(){
    translate([0,0,i])
      cylinder(.1,d1=10*sin(i*9),d2=0);
    translate([0,0,j])
      cylinder(.1,d1=10*sin(j*9),d2=0);
  }
}

将文本适应到给定区域

[编辑 | 编辑源代码]

目前没有办法查询text()生成的几何图形的大小。根据模型,可能可以计算文本大小的粗略估计,并将文本适应到已知区域。这使用resize()来实现,假设长度是主要值。

OpenSCAD - 将文本适应到给定区域
// Generate 2 random values between 10 and 30
r = rands(10, 30, 2);

// Calculate width and length from random values
width = r[1];
length = 3 * r[0];

difference() {
    // Create border
    linear_extrude(2, center = true)
        square([length + 4, width + 4], center = true);
    // Cut the area for the text
    linear_extrude(2)
        square([length + 2, width + 2], center = true);
    // Fit the text into the area based on the length
    color("green")
        linear_extrude(1.5, center = true, convexity = 4)
                resize([length, 0], auto = true)
                    text("Text goes here!", valign = "center", halign = "center");
}

创建镜像对象同时保留原始对象

[编辑 | 编辑源代码]

mirror()模块只会转换现有的对象,因此它不能用于生成对称对象。但是,使用children()模块,可以很容易地定义一个新的模块mirror_copy(),除了原始对象之外,还可以生成镜像对象。

OpenSCAD - 镜像复制
// A custom mirror module that retains the original
// object in addition to the mirrored one.
module mirror_copy(v = [1, 0, 0]) {
    children();
    mirror(v) children();
}

// Define example object.
module object() {
    translate([5, 5, 0]) {
        difference() {
            cube(10);
            cylinder(r = 8, h = 30, center = true);
        }
    }
}

// Call mirror_copy twice, once using the default to
// create a duplicate mirrored on X axis and
// then mirror again on Y axis.
mirror_copy([0, 1, 0])
    mirror_copy()
        object();

在空间数组上排列部件

[编辑 | 编辑源代码]

一个用于在数组上显示一组对象的运算符。

OpenSCAD - 对象数组
 // Arrange its children in a regular rectangular array
 //      spacing - the space between children origins
 //      n       - the number of children along x axis
 module arrange(spacing=50, n=5) {
    nparts = $children;
    for(i=[0:1:n-1], j=[0:nparts/n])
        if (i+n*j < nparts)
            translate([spacing*(i+1), spacing*j, 0]) 
                children(i+n*j);
 }

 arrange(spacing=30,n=3) {
    sphere(r=20,$fn=8);
    sphere(r=20,$fn=10);
    cube(30,center=true);
    sphere(r=20,$fn=14);
    sphere(r=20,$fn=16);
    sphere(r=20,$fn=18);
    cylinder(r=15,h=30);
    sphere(r=20,$fn=22);
 }

一个方便的运算符,用于显示从Thingiverse下载的项目的大量部件。

注意:以下用法失败

 arrange() for(i=[8:16]) sphere(15, $fn=i);

因为for语句对内部对象执行隐式联合,只创建一个子对象。

圆角多边形

[编辑 | 编辑源代码]

多边形可以通过偏移运算符以多种形式进行圆角处理。

OpenSCAD 偏移运算符 offset() 对多边形的圆角处理
 p = [ [0,0], [10,0], [10,10], [5,5], [0,10]];

 polygon(p);
 // round pointed vertices and enlarge
 translate([-15, 0])
 offset(1,$fn=24) polygon(p);
 // round concavities and shrink
 translate([-30, 0])
 offset(-1,$fn=24) polygon(p);
 // round concavities and preserve polygon dimensions
 translate([15, 0])
 offset(-1,$fn=24) offset(1,$fn=24) polygon(p);
 // round pointed vertices and preserve polygon dimensions
 translate([30, 0])
 offset(1,$fn=24) offset(-1,$fn=24) polygon(p);
 // round all vertices and preserve polygon dimensions
 translate([45, 0])
 offset(-1,$fn=24) offset(1,$fn=24) 
 offset(1,$fn=24) offset(-1,$fn=24) polygon(p);

倒角对象

[编辑 | 编辑源代码]

倒角是多边形圆角的 3D 对应物。3D 对象没有 offset() 运算符,但可以使用 minkowski 运算符进行编码。

OpenSCAD - 倒角对象
 difference(){
     offset_3d(2) offset_3d(-2) // exterior fillets
         offset_3d(-4) offset_3d(4) // interior fillets
             basic_model();
     // hole without fillet
     translate([0,0,10]) 
         cylinder(r=18,h=50);
 }
 // simpler (faster) example of a negative offset
* offset_3d(-4)difference(){
     cube(50,center=true);
     cube(50,center=false);
 }

 module basic_model(){
     cylinder(r=25,h=55,$fn=6);// $fn=6 for faster calculation
     cube([80,80,10], center=true);
 }

 module offset_3d(r=1, size=1000) {
     n = $fn==undef ? 12: $fn;
     if(r==0) children();
     else 
         if( r>0 )
             minkowski(convexity=5){
                 children();
                 sphere(r, $fn=n);
             }
         else {
             size2 = size*[1,1,1];// this will form the positv
             size1 = size2*2;    // this will hold a negative inside
             difference(){
                 cube(size2, center=true);// forms the positiv by substracting the negative
                 minkowski(convexity=5){
                     difference(){
                         cube(size1, center=true);
                         children();
                     }
                     sphere(-r, $fn=n);
                 }
             }
         }
 }

请注意,这是一个非常耗时的过程。minkowski 运算符会向模型添加顶点,因此每个新的 offset_3d 都比前一个花费更长时间。

计算边界框

[编辑 | 编辑源代码]

无法使用 OpenSCAD 获取对象的边界框限制。但是,可以计算其边界框体积。其概念很简单:对模型在每个轴上的投影(1D 集)进行 hull() 操作,并对它们进行 minkowski() 操作。由于无法在 OpenSCAD 中定义 1D 集,因此投影由长度等于投影大小的棍子近似表示。

module bbox() { 

    // a 3D approx. of the children projection on X axis 
    module xProjection() 
        translate([0,1/2,-1/2]) 
            linear_extrude(1) 
                hull() 
                    projection() 
                        rotate([90,0,0]) 
                            linear_extrude(1) 
                                projection() children(); 
  
    // a bounding box with an offset of 1 in all axis
    module bbx()  
        minkowski() { 
            xProjection() children(); // x axis
            rotate(-90)               // y axis
                xProjection() rotate(90) children(); 
            rotate([0,-90,0])         // z axis
                xProjection() rotate([0,90,0]) children(); 
        } 
    
    // offset children() (a cube) by -1 in all axis
    module shrink()
      intersection() {
        translate([ 1, 1, 1]) children();
        translate([-1,-1,-1]) children();
      }

   shrink() bbx() children(); 
}
OpenSCAD - 对象的边界框

图像显示了代码生成的红色模型的(透明)边界框。

module model()
  color("red") 
  union() {
    sphere(10);
    translate([15,10,5]) cube(10);
  }

model();
%bbox() model();

倒角对象技巧的 offset3D 运算符代码中的立方体可以很好地替换为对象的边界框,从而省去人工参数大小。

OpenSCAD - 文本边界框操作

作为解决此类问题的示例,通过对结果进行少量操作,可以将边界框用于在任意文本周围增强特征,而无需了解文本的大小。在此示例中,为文本创建了一个方形底板,并在文本的两端插入了两个孔,所有这些都具有固定的边距。这通过获取边界框的投影、均匀扩展它、将 y 维度缩小到一个细条并向外扩展 x 方向来实现,然后再次减去扩展的边界框投影,留下两个接近点状的对象,这些对象可以使用偏移扩展成孔。

my_string = "Demo text";

module BasePlate(margin) {
  minkowski() {
    translate(-margin) square(2*margin);
    projection() bbox() linear_extrude(1) children();
  }
}

module TextThing() {
  text(my_string, halign="center", valign="center");
}


hole_size = 3;
margwidth = 2;
linear_extrude(1)
  difference() {
    BasePlate([2*(hole_size+margwidth), margwidth]) TextThing();
    offset(hole_size) {
      difference() {
        scale([1.001, 1])
          resize([-1, 0.001])
          BasePlate([hole_size+margwidth, margwidth]) TextThing();
        BasePlate([hole_size+margwidth, margwidth]) TextThing();
      }
    }
  }

linear_extrude(2) TextThing();

数据高度图

[编辑 | 编辑源代码]

内置模块 surface() 能够创建一个 3D 对象,该对象表示数字矩阵中数据的等高线。但是,surface() 的数据矩阵应存储在外部文本文件中。以下模块为用户代码生成的数据集执行 surface() 的精确等高线。

OpenSCAD - 由 surfaceData() 生成的等高线
data = [ for(a=[0:10:360])
          [ for(b=[0:10:360])
              cos(a-b)+4*sin(a+b)+(a+b)/40 ]
       ];

surfaceData(data, center=true);
cube();

// operate like the builtin module surface() but
// from a matrix of floats instead of a text file
module surfaceData(M, center=false, convexity=10){
  n = len(M);
  m = len(M[0]);
  miz  = min([for(Mi=M) min(Mi)]);
  minz = miz<0? miz-1 : -1;
  ctr  = center ? [-(m-1)/2, -(n-1)/2, 0]: [0,0,0];
  points = [ // original data points
             for(i=[0:n-1])for(j=[0:m-1]) [j, i, M[i][j]] +ctr,
             [   0,   0, minz ] + ctr, 
             [ m-1,   0, minz ] + ctr, 
             [ m-1, n-1, minz ] + ctr, 
             [   0, n-1, minz ] + ctr,
             // additional interpolated points at the center of the quads
             // the points bellow with `med` set to 0 are not used by faces
             for(i=[0:n-1])for(j=[0:m-1])
               let( med = i==n-1 || j==m-1 ? 0:
                          (M[i][j]+M[i+1][j]+M[i+1][j+1]+M[i][j+1])/4 )
               [j+0.5, i+0.5, med] + ctr
           ];
  faces = [ // faces connecting data points to interpolated ones
            for(i=[0:n-2])
              for(j=[i*m:i*m+m-2]) 
                each [ [   j+1,     j, j+n*m+4 ], 
                       [     j,   j+m, j+n*m+4 ], 
                       [   j+m, j+m+1, j+n*m+4 ], 
                       [ j+m+1,   j+1, j+n*m+4 ] ] ,
            // lateral and bottom faces
            [ for(i=[0:m-1])           i, n*m+1,   n*m ], 
            [ for(i=[m-1:-1:0]) -m+i+n*m, n*m+3, n*m+2 ], 
            [ for(i=[n-1:-1:0])      i*m,   n*m, n*m+3 ], 
            [ for(i=[0:n-1])     i*m+m-1, n*m+2, n*m+1 ],
            [n*m, n*m+1, n*m+2, n*m+3 ]
        ];
  polyhedron(points, faces, convexity);
}

字符串

[编辑 | 编辑源代码]

来自数字字符串的整数(十进制或十六进制)

[编辑 | 编辑源代码]

将字符串格式的数字转换为整数,(s2d - 字符串到十进制 - 在我添加十六进制之前命名为…)

例如 echo(s2d("314159")/100000); // 显示 ECHO: 3.14159

function s2d(h="0",base=10,i=-1) =
// converts a string of hexa/or/decimal digits into a decimal 
// integers only
	(i == -1)
	? s2d(h,base,i=len(h)-1)
	: 	(i == 0)
		? _chkBase(_d2n(h[0]),base)
		: _chkBase(_d2n(h[i]),base) + base*s2d(h,base,i-1);

function _chkBase(n,b) = 
	(n>=b)
	? (0/0)		// 0/0=nan
	: n;
    

function _d2n(digitStr) =
// SINGLE string Digit 2 Number, decimal (0-9) or hex (0-F) - upper or lower A-F
	(digitStr == undef 
				|| len(digitStr) == undef 
				|| len(digitStr) != 1)
	? (0/0) // 0/0 = nan
	: _d2nV()[search(digitStr,_d2nV(),1,0)[0]][1];

function _d2nV()= 
// Digit 2 Number Vector, use function instead of variable - no footprints
  [	["0",0],["1",1],["2",2],["3",3],["4",4],
		["5",5],["6",6],["7",7],["8",8],["9",9],
		["a",10],["b",11],["c",12],
		["d",13],["e",14],["f",15],
		["A",10],["B",11],["C",12],
		["D",13],["E",14],["F",15]
	];

调试 Tap 函数

[编辑 | 编辑源代码]

类似于 Ruby 的 Tap 函数。如果 $_debug 为真,则此函数封装到控制台的 echo 副作用并返回对象。

例如,给定 $_debug 为真;x = debugTap(2 * 2, "Solution is: "); // 显示 ECHO: Solution is: 4

function debugTap(o, s) = let(
  nothing = [ for (i = [1:1]) if ($_debug) echo(str(s, ": ", o)) ]) o;

// usage
// note: parseArgsToString() just concats all the args and returns a pretty str

$_debug = true;

// doubles 'x'
function foo(x) =
  let(
   fnName = "foo",
   args = [x]
 )
 debugTap(x * x, str(fnName, parseArgsToString(args)));

x = 2;
y = foo(x);

echo(str("x: ", x, " y: ", y));

// console display:
// ECHO: "foo(2): 4"
// ECHO: "x: 2 y: 4"
华夏公益教科书