tkz-elements 宏包介绍

1. tkz-euclide 宏包的优缺点

之前在绘制平面几何图形的时候,一直使用 tkz-euclide 宏包。其优点如下:

  • 方便调整图像大小和形状;
  • 常见的需求都能够直接画出来(如三角形的各种心);
  • 函数的命名规则比较规矩;
  • 整个绘制图形的代码分为三部分,可读性很好:
    • 定义(如 \tkzDef...\tkzInter...);
    • 绘制(如 \tkzDraw...\tkzFill...);
    • 标记(如 \tkzLabel...\tkzMark...)。

但是缺点也很明显:

  • 命名比较繁琐;
  • 各种括号容易写混了;
  • 各个参数的顺序很不自然。

例如「直线 AIAIO\odot O 异于 AA 的交点为 PP」,用 tkz-euclide 命令来写是

1
\tkzInterLC[common=A](A,I)(O,A) \tkzGetFirstPoint{P}

这里面主要有两个问题,一是可选参数 [common=A] 在中间,而正常的思路是先写出直线和圆,再写可选参数,这样会比较顺;二则是后面的获取交点 PP,需要一个单独的命令,而且还要根据前面可选参数的不同,来决定是使用 \tkzGetFirstPoint 还是 \tkzGetPoints

另外,一个命令里同时用到了小括号、中括号、大括号,虽然是有规律的,但是还是容易打错了,导致编译失败。

当然解决方法也是有的,那就是利用 VSCode 的代码片段(snippet)功能,这样可以自定义参数的输入顺序,也可以保证不会打错括号。例如上面的命令,实际上用到了下面的代码片段:

1
2
3
4
5
6
7
"tkzInterLC1": {
"prefix": "\\tkzInterLC",
"body": [
"\\tkzInterLC[${3}]($1)($2) \\tkzGetFirstPoint{$4}",
"$0"
]
}

我把常用的 50 多个命令都写成了类似上面的代码片段,这样在使用的时候就能够自动补全,只需要输入对应的参数即可。

2. tkz-elements 宏包简介

今年在写几何题解析的时候,发现了 tkz-euclide 宏包作者写的新宏包:tkz-elements,把比较繁琐的「定义」的部分改为使用 lua 语言进行编程。这样写出的代码可读性更强,写起来也更自然。

主要改进的点有:

  • 计算更加精确(不过这点对我实际使用影响不大);
  • 使用面向对象编程的范式,这对于写惯了编程代码的我来说就很顺了。

以 2024 年 IMO 第 4 题为例,定义部分新旧代码的对比如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
%% 定义三角形 ABC
\def\r{4}
\tkzDefPoint(120:\r){A}
\tkzDefPoint(190:\r){B}
\tkzDefPoint(350:\r){C}

%% 定义内心、外心、内切圆切点
\tkzDefTriangleCenter[in](A,B,C) \tkzGetPoint{I}
\tkzDefTriangleCenter[circum](A,B,C) \tkzGetPoint{O}
\tkzDefSpcTriangle[intouch, name=T_](A,B,C){A,B,C}

%% 定义 X、Y 两点
\tkzDefPointsBy[symmetry=center I](T_B,T_C){B',C'}
\tkzDefTangent[at=B'](I) \tkzGetPoint{B''}
\tkzInterLL(B',B'')(B,C) \tkzGetPoint{X}
\tkzInterLL(B',B'')(A,B) \tkzGetPoint{X'}

\tkzDefTangent[at=C'](I) \tkzGetPoint{C''}
\tkzInterLL(C',C'')(B,C) \tkzGetPoint{Y}
\tkzInterLL(C',C'')(A,C) \tkzGetPoint{Y'}

%% 定义其它点
\tkzInterLC[common=A](A,I)(O,A) \tkzGetFirstPoint{P}
\tkzDefMidPoint(A,C) \tkzGetPoint{K}
\tkzDefMidPoint(A,B) \tkzGetPoint{L}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- 定义三角形 ABC
local dis = 4
z.A = point:polar_deg(dis, 120)
z.B = point:polar_deg(dis, 190)
z.C = point:polar_deg(dis, 350)
T.ABC = triangle(z.A, z.B, z.C)

-- 定义内心、外心、内切圆切点
z.O = T.ABC.circumcenter
C.I = T.ABC:in_circle()
z.I = C.I.center
z.T_A, z.T_B, z.T_C = T.ABC:intouch():get()

-- 定义 X、Y 两点
z.Bp = C.I:antipode(z.T_B)
L.BpX = C.I:tangent_at(z.Bp)
z.X = intersection(L.BpX, T.ABC.bc)
z.Cp = C.I:antipode(z.T_C)
L.CpY = C.I:tangent_at(z.Cp)
z.Y = intersection(L.CpY, T.ABC.bc)
z.Xp = intersection(L.BpX, T.ABC.ab)
z.Yp = intersection(L.CpY, T.ABC.ca)

-- 定义其它点
z.P = intersection(T.ABC:bisector(), T.ABC:circum_circle(), {known = z.A})
_, z.K, z.L = T.ABC:medial():get()

使用 tkz-elements 绘制整个图形的代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
\directlua{
init_elements()
-- 上面的定义部分...
}

\begin{tikzpicture}
\tkzGetNodes

%% 画图部分...
%% 依旧使用 tkz-euclide 宏包
\end{tikzpicture}

首先使用 \directlua 导入 lua 代码。

这也决定了该文档必须使用 LuaLaTeX 编译,而不能使用 pdfLaTeX 或 XeLaTeX 编译。
如果使用 latexmk 控制编译过程,加上 -pdflua 参数即可。

然后在 tikzpicture 环境中使用 \tkzGetNodes 获取上面定义的几何对象。

tkz-elements 宏包使用 lua 语言的 table(表)来保存几何对象,常用的有:

  • z:点(复数 zz
  • L:直线(line)
  • C:圆(circle)
  • T:三角形(triangle)

其它还包括:

  • Q:四边形(quadrilateral)
  • P:平行四边形(parallelogram)
  • R:矩形(rectangle)
  • S:正方形(square)
  • RP:正多边形(regular polygon)
  • O:直角坐标系(orthonormal Cartesian coordinate system)
  • V:向量(vector)
  • M:矩阵(matrix)
  • PA:路径(path)

上面代码中的 init_elements(),其实就是初始化(或者重置)上面这些储存几何对象的表。

lua 使用 table.name 来访问表中的元素(当然也可以使用 table["name"],不过显然前者更简洁)。

例如,z.A 表示点 AAz.T_A 表示点 TAT_Az.Bp 表示点 BB'(撇号的英文是 prime),T.ABC 表示三角形 ABCABC

lua 可以通过 table(以及内部的 metatable)实现面向对象编程。我们知道,对象由属性(attribute)和方法(method)组成。

lua 使用点号 . 来访问属性,使用冒号 : 来访问方法。

值得说明的是,有两个方法是所有几何对象都有的:new(...) 方法用来构造新的对象,get() 方法用来获取构造对象的点。

以三角形为例,我们首先使用 new 方法来创建对象:

1
2
3
T.ABC = triangle:new(z.A, z.B, z.C)
-- 或者简化写法
T.ABC = triangle(z.A, z.B, z.C)

三角形包含一些常见的属性,比如顶点、边、面积、半周长、内心、外心、内径、外径等等。包含的方法也很多,包括中线、高、角平分线、内切圆、外接圆等等,基本上经常用到的都有定义。例如

1
2
3
4
5
6
-- 点 O 是三角形 ABC 的外心
z.O = T.ABC.circumcenter
-- 圆 I 是三角形 ABC 的内切圆
C.I = T.ABC:in_circle()
-- 获取三角形 ABC 的内切点三角形的三个顶点
z.T_A, z.T_B, z.T_C = T.ABC:intouch():get()

这些属性和方法基本上就是对应的英文名称,因此很好记。如果不确定的话,文档查起来也很方便。在文档中,每种几何对象都有一个列表,列出所有的属性;然后所有方法按照返回值的类型分别举例说明。

3. tkz-elements 宏包的一些内部实现

由于 tkz-elements 宏包是使用 lua 语言实现的,因此我能够看懂它的源代码,可以了解它的具体实现方法。

3.1. tkz-elements.sty

加载 tkz_elements_main 模块,定义了 tkzelements 环境、\tkzGetNodes 命令,还有一些不太常用的命令。

3.2. tkz_elements_main.lua

加载各个几何对象对应的定义和函数模块,定义了 init_elements 初始化函数。

3.3. tkz_elements_point.lua

首先定义了一个工厂函数 class,用来实现面向对象的类,然后使用复数类来定义点。

3.4. tkz_elements_triangle.lua

可以看到,三角形的各个心都是通过重心坐标或者三线坐标计算得到。