@lydt/geogebra-studio 0.0.0-stage → 0.4.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.agents/plugins/marketplace.json +21 -0
  2. package/.codex-plugin/plugin.json +21 -0
  3. package/.mcp.json +18 -0
  4. package/LICENSE +339 -0
  5. package/LICENSE-MIT +21 -0
  6. package/README.md +108 -2
  7. package/README.zh-CN.md +89 -0
  8. package/THIRD_PARTY_NOTICES.md +35 -0
  9. package/dist/cli.d.ts +27 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +234 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/export/files.d.ts +4 -0
  14. package/dist/export/files.d.ts.map +1 -0
  15. package/dist/export/files.js +44 -0
  16. package/dist/export/files.js.map +1 -0
  17. package/dist/export/gikz.d.ts +11 -0
  18. package/dist/export/gikz.d.ts.map +1 -0
  19. package/dist/export/gikz.js +28 -0
  20. package/dist/export/gikz.js.map +1 -0
  21. package/dist/export/html.d.ts +14 -0
  22. package/dist/export/html.d.ts.map +1 -0
  23. package/dist/export/html.js +97 -0
  24. package/dist/export/html.js.map +1 -0
  25. package/dist/export/semantic.d.ts +43 -0
  26. package/dist/export/semantic.d.ts.map +1 -0
  27. package/dist/export/semantic.js +212 -0
  28. package/dist/export/semantic.js.map +1 -0
  29. package/dist/export/viewer-themes.d.ts +76 -0
  30. package/dist/export/viewer-themes.d.ts.map +1 -0
  31. package/dist/export/viewer-themes.js +169 -0
  32. package/dist/export/viewer-themes.js.map +1 -0
  33. package/dist/export/workspace.d.ts +4 -0
  34. package/dist/export/workspace.d.ts.map +1 -0
  35. package/dist/export/workspace.js +25 -0
  36. package/dist/export/workspace.js.map +1 -0
  37. package/dist/gikz-browser.js +770 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +84 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/server.d.ts +35 -0
  43. package/dist/server.d.ts.map +1 -0
  44. package/dist/server.js +187 -0
  45. package/dist/server.js.map +1 -0
  46. package/dist/studio/artifact.d.ts +37 -0
  47. package/dist/studio/artifact.d.ts.map +1 -0
  48. package/dist/studio/artifact.js +61 -0
  49. package/dist/studio/artifact.js.map +1 -0
  50. package/dist/studio/browser.d.ts +7 -0
  51. package/dist/studio/browser.d.ts.map +1 -0
  52. package/dist/studio/browser.js +67 -0
  53. package/dist/studio/browser.js.map +1 -0
  54. package/dist/studio/install.d.ts +4 -0
  55. package/dist/studio/install.d.ts.map +1 -0
  56. package/dist/studio/install.js +60 -0
  57. package/dist/studio/install.js.map +1 -0
  58. package/dist/tools/apps-api-tools.d.ts +4 -0
  59. package/dist/tools/apps-api-tools.d.ts.map +1 -0
  60. package/dist/tools/apps-api-tools.js +96 -0
  61. package/dist/tools/apps-api-tools.js.map +1 -0
  62. package/dist/tools/educational-templates.d.ts +33 -0
  63. package/dist/tools/educational-templates.d.ts.map +1 -0
  64. package/dist/tools/educational-templates.js +261 -0
  65. package/dist/tools/educational-templates.js.map +1 -0
  66. package/dist/tools/export-tools.d.ts +3 -0
  67. package/dist/tools/export-tools.d.ts.map +1 -0
  68. package/dist/tools/export-tools.js +118 -0
  69. package/dist/tools/export-tools.js.map +1 -0
  70. package/dist/tools/geogebra-tools.d.ts +15 -0
  71. package/dist/tools/geogebra-tools.d.ts.map +1 -0
  72. package/dist/tools/geogebra-tools.js +3442 -0
  73. package/dist/tools/geogebra-tools.js.map +1 -0
  74. package/dist/tools/index.d.ts +43 -0
  75. package/dist/tools/index.d.ts.map +1 -0
  76. package/dist/tools/index.js +183 -0
  77. package/dist/tools/index.js.map +1 -0
  78. package/dist/tools/performance-tools.d.ts +7 -0
  79. package/dist/tools/performance-tools.d.ts.map +1 -0
  80. package/dist/tools/performance-tools.js +312 -0
  81. package/dist/tools/performance-tools.js.map +1 -0
  82. package/dist/tools/templates/algebra-templates.d.ts +7 -0
  83. package/dist/tools/templates/algebra-templates.d.ts.map +1 -0
  84. package/dist/tools/templates/algebra-templates.js +568 -0
  85. package/dist/tools/templates/algebra-templates.js.map +1 -0
  86. package/dist/tools/templates/geometry-templates.d.ts +7 -0
  87. package/dist/tools/templates/geometry-templates.d.ts.map +1 -0
  88. package/dist/tools/templates/geometry-templates.js +492 -0
  89. package/dist/tools/templates/geometry-templates.js.map +1 -0
  90. package/dist/types/geogebra.d.ts +103 -0
  91. package/dist/types/geogebra.d.ts.map +1 -0
  92. package/dist/types/geogebra.js +34 -0
  93. package/dist/types/geogebra.js.map +1 -0
  94. package/dist/types/mcp.d.ts +125 -0
  95. package/dist/types/mcp.d.ts.map +1 -0
  96. package/dist/types/mcp.js +19 -0
  97. package/dist/types/mcp.js.map +1 -0
  98. package/dist/utils/app-info.d.ts +12 -0
  99. package/dist/utils/app-info.d.ts.map +1 -0
  100. package/dist/utils/app-info.js +30 -0
  101. package/dist/utils/app-info.js.map +1 -0
  102. package/dist/utils/errors.d.ts +37 -0
  103. package/dist/utils/errors.d.ts.map +1 -0
  104. package/dist/utils/errors.js +72 -0
  105. package/dist/utils/errors.js.map +1 -0
  106. package/dist/utils/gemini-compatibility.d.ts +39 -0
  107. package/dist/utils/gemini-compatibility.d.ts.map +1 -0
  108. package/dist/utils/gemini-compatibility.js +154 -0
  109. package/dist/utils/gemini-compatibility.js.map +1 -0
  110. package/dist/utils/geogebra-instance.d.ts +252 -0
  111. package/dist/utils/geogebra-instance.d.ts.map +1 -0
  112. package/dist/utils/geogebra-instance.js +1560 -0
  113. package/dist/utils/geogebra-instance.js.map +1 -0
  114. package/dist/utils/geogebra-mock.d.ts +172 -0
  115. package/dist/utils/geogebra-mock.d.ts.map +1 -0
  116. package/dist/utils/geogebra-mock.js +732 -0
  117. package/dist/utils/geogebra-mock.js.map +1 -0
  118. package/dist/utils/logger.d.ts +4 -0
  119. package/dist/utils/logger.d.ts.map +1 -0
  120. package/dist/utils/logger.js +46 -0
  121. package/dist/utils/logger.js.map +1 -0
  122. package/dist/utils/performance/index.d.ts +76 -0
  123. package/dist/utils/performance/index.d.ts.map +1 -0
  124. package/dist/utils/performance/index.js +163 -0
  125. package/dist/utils/performance/index.js.map +1 -0
  126. package/dist/utils/performance/instance-pool.d.ts +78 -0
  127. package/dist/utils/performance/instance-pool.d.ts.map +1 -0
  128. package/dist/utils/performance/instance-pool.js +268 -0
  129. package/dist/utils/performance/instance-pool.js.map +1 -0
  130. package/dist/utils/validation.d.ts +92 -0
  131. package/dist/utils/validation.d.ts.map +1 -0
  132. package/dist/utils/validation.js +557 -0
  133. package/dist/utils/validation.js.map +1 -0
  134. package/dist/widget.html +110 -0
  135. package/docs/APPS-API-BRIDGE.md +28 -0
  136. package/docs/MODERNIZATION.md +15 -0
  137. package/docs/TANGENT-REGRESSION.md +34 -0
  138. package/docs/VALIDATION.md +26 -0
  139. package/docs/VISUAL-STANDARD.md +26 -0
  140. package/docs/images/native-construction.png +0 -0
  141. package/examples/tangent-construction.json +26 -0
  142. package/gikz/LICENSE +339 -0
  143. package/gikz/PROVENANCE.md +8 -0
  144. package/gikz/browser.cjs +2 -0
  145. package/gikz/cli.cjs +195 -0
  146. package/gikz/core.cjs +806 -0
  147. package/mcp.json +20 -0
  148. package/package.json +107 -4
  149. package/plugin.json +35 -0
  150. package/runtime/package.json +4 -0
  151. package/scripts/build-plugin.cjs +15 -0
  152. package/scripts/build-widget.cjs +7 -0
  153. package/skills/geogebra-construct/SKILL.md +40 -0
  154. package/skills/geogebra-setup/SKILL.md +37 -0
  155. package/skills/geometry-export/SKILL.md +31 -0
  156. package/src/cli.ts +212 -0
  157. package/src/export/files.ts +26 -0
  158. package/src/export/gikz.ts +22 -0
  159. package/src/export/html.ts +108 -0
  160. package/src/export/semantic.ts +180 -0
  161. package/src/export/viewer-themes.ts +165 -0
  162. package/src/export/workspace.ts +15 -0
  163. package/src/index.ts +71 -0
  164. package/src/server.ts +168 -0
  165. package/src/studio/artifact.ts +50 -0
  166. package/src/studio/browser.ts +38 -0
  167. package/src/studio/install.ts +43 -0
  168. package/src/tools/apps-api-tools.ts +96 -0
  169. package/src/tools/educational-templates.ts +299 -0
  170. package/src/tools/export-tools.ts +110 -0
  171. package/src/tools/geogebra-tools.ts +3836 -0
  172. package/src/tools/index.ts +192 -0
  173. package/src/tools/performance-tools.ts +322 -0
  174. package/src/tools/templates/algebra-templates.ts +619 -0
  175. package/src/tools/templates/geometry-templates.ts +550 -0
  176. package/src/types/geogebra.ts +140 -0
  177. package/src/types/global.d.ts +31 -0
  178. package/src/types/mcp.ts +141 -0
  179. package/src/utils/app-info.ts +35 -0
  180. package/src/utils/errors.ts +97 -0
  181. package/src/utils/gemini-compatibility.ts +193 -0
  182. package/src/utils/geogebra-instance.ts +1712 -0
  183. package/src/utils/geogebra-mock.ts +867 -0
  184. package/src/utils/logger.ts +60 -0
  185. package/src/utils/performance/index.ts +211 -0
  186. package/src/utils/performance/instance-pool.ts +323 -0
  187. package/src/utils/validation.ts +647 -0
  188. package/tsconfig.json +35 -0
  189. package/widget/main.ts +12 -0
package/gikz/core.cjs ADDED
@@ -0,0 +1,806 @@
1
+ #!/usr/bin/env node
2
+ // Gikz — Clean GeoGebra TikZ exports into concise, readable LaTeX code
3
+ //
4
+ // Derived from GGB-Tikz-Code-Filter by By990920
5
+ // Original: https://github.com/By990920/GGB-Tikz-Code-Filter
6
+ //
7
+ // Copyright (C) 2026
8
+ // This program is free software; you can redistribute it and/or modify
9
+ // it under the terms of the GNU General Public License as published by
10
+ // the Free Software Foundation; either version 2 of the License, or
11
+ // (at your option) any later version.
12
+ //
13
+ // 用法:
14
+ // node gikz.js [选项] <文件...>
15
+ // cat export.txt | node gikz.js [选项]
16
+ //
17
+ // 选项:
18
+ // --standalone, -s 输出完整 standalone LaTeX 文档
19
+ // --tikzonly, -t 仅输出 tikzpicture 片段(默认)
20
+ // --no-points 不输出点标记
21
+ // --no-labels 不输出点标签
22
+ // --no-round 不四舍五入坐标
23
+ // --output, -o <file> 输出到文件(批量时为目录)
24
+ // --help, -h 显示帮助
25
+
26
+ 'use strict';
27
+
28
+ function roundToThreeDecimals(num) {
29
+ return Math.round(num * 1000) / 1000;
30
+ }
31
+
32
+ function formatCoordinate(coordStr, shouldRound) {
33
+ const match = coordStr.match(/\(([^,]+),([^)]+)\)/);
34
+ if (!match) return coordStr;
35
+ let x = parseFloat(match[1]);
36
+ let y = parseFloat(match[2]);
37
+ if (shouldRound) {
38
+ x = roundToThreeDecimals(x);
39
+ y = roundToThreeDecimals(y);
40
+ }
41
+ return `(${x},${y})`;
42
+ }
43
+
44
+ function getSmartLabelPosition(x, y, allPoints) {
45
+ const xCoords = allPoints.map(p => p.x);
46
+ const yCoords = allPoints.map(p => p.y);
47
+ const minX = Math.min(...xCoords);
48
+ const maxX = Math.max(...xCoords);
49
+ const minY = Math.min(...yCoords);
50
+ const maxY = Math.max(...yCoords);
51
+ const centerX = (minX + maxX) / 2;
52
+ const centerY = (minY + maxY) / 2;
53
+
54
+ let position = 'above';
55
+ if (Math.abs(x - minX) < 0.1) {
56
+ position = 'left';
57
+ } else if (Math.abs(x - maxX) < 0.1) {
58
+ position = 'right';
59
+ } else if (Math.abs(y - minY) < 0.1) {
60
+ position = 'below';
61
+ } else if (Math.abs(y - maxY) < 0.1) {
62
+ position = 'above';
63
+ } else {
64
+ if (x > centerX && y > centerY) position = 'above right';
65
+ else if (x > centerX && y <= centerY) position = 'below right';
66
+ else if (x <= centerX && y > centerY) position = 'above left';
67
+ else position = 'below left';
68
+ }
69
+ return position;
70
+ }
71
+
72
+ const convertLineStyle = (style) => {
73
+ if (!style) return '';
74
+ let lineStyle = style.replace(/line width=[^,\]]+,?/g, '');
75
+ lineStyle = lineStyle.replace(/,\s*$/g, '');
76
+ lineStyle = lineStyle.replace(/\[\s*,\s*/g, '[');
77
+ if (lineStyle.includes('dash pattern=on 1pt off 1pt on 1pt off 4pt')) return 'dash dot';
78
+ if (lineStyle.includes('dash pattern=on 1pt off 1pt')) return 'dashed';
79
+ if (lineStyle.includes('dotted')) return 'dotted';
80
+ if (lineStyle.includes('dash')) return 'dashed';
81
+ return '';
82
+ };
83
+
84
+ // ─── 提取函数 ───────────────────────────────────────────
85
+
86
+ function extractQuadraticFunctions(code, shouldRound) {
87
+ try {
88
+ const quadraticRegex = /\\draw\s*\[([^\]]+)\]\s*plot\s*\(\\x,\{\(\\x\)\^2\/2\/([^}]+)\}\)/g;
89
+ const quadraticMatches = [...code.matchAll(quadraticRegex)];
90
+ const quadraticFunctions = [];
91
+
92
+ quadraticMatches.forEach(match => {
93
+ const options = match[1];
94
+ let denominator = match[2];
95
+
96
+ const samplesMatch = options.match(/samples=(\d+)/);
97
+ const rotateMatch = options.match(/rotate around=\{([^:]+):\(([^,]+),([^)]+)\)\}/);
98
+ const xshiftMatch = options.match(/xshift=([^,]+)cm/);
99
+ const yshiftMatch = options.match(/yshift=([^,]+)cm/);
100
+ const domainMatch = options.match(/domain=([^:)]+):([^)]+)\)?/);
101
+
102
+ if (domainMatch) {
103
+ let angle = "0", centerX = "0", centerY = "0";
104
+ if (rotateMatch) { angle = rotateMatch[1]; centerX = rotateMatch[2]; centerY = rotateMatch[3]; }
105
+ let xshift = "0", yshift = "0";
106
+ if (xshiftMatch) xshift = xshiftMatch[1];
107
+ if (yshiftMatch) yshift = yshiftMatch[1];
108
+ let domainStart = domainMatch[1];
109
+ let domainEnd = domainMatch[2];
110
+ if (domainEnd.includes(')')) domainEnd = domainEnd.replace(')', '');
111
+
112
+ if (shouldRound) {
113
+ if (centerX !== "0") centerX = roundToThreeDecimals(parseFloat(centerX));
114
+ if (centerY !== "0") centerY = roundToThreeDecimals(parseFloat(centerY));
115
+ if (xshift !== "0") xshift = roundToThreeDecimals(parseFloat(xshift));
116
+ if (yshift !== "0") yshift = roundToThreeDecimals(parseFloat(yshift));
117
+ denominator = roundToThreeDecimals(parseFloat(denominator));
118
+ domainStart = roundToThreeDecimals(parseFloat(domainStart));
119
+ domainEnd = roundToThreeDecimals(parseFloat(domainEnd));
120
+ xshift += 'cm'; yshift += 'cm';
121
+ }
122
+
123
+ let newOptions = 'smooth,samples=500';
124
+ if (rotateMatch) newOptions += `,rotate around={${angle}:(${centerX},${centerY})}`;
125
+ if ((xshiftMatch && xshift !== "0cm") || (yshiftMatch && yshift !== "0cm"))
126
+ newOptions += `,xshift=${xshift},yshift=${yshift}`;
127
+ newOptions += `,domain=${domainStart}:${domainEnd}`;
128
+
129
+ quadraticFunctions.push({ original: match[0], options: newOptions, expression: `(\\x)^2/2/${denominator}` });
130
+ }
131
+ });
132
+ return { hasQuadratic: quadraticFunctions.length > 0, quadraticFunctions };
133
+ } catch (error) {
134
+ return { hasQuadratic: false, quadraticFunctions: [] };
135
+ }
136
+ }
137
+
138
+ function extractFunctionPlots(code, shouldRound) {
139
+ try {
140
+ const clipRegex = /\\clip\(([^,]+),([^)]+)\)\s*rectangle\s*\(([^,]+),([^)]+)\);/;
141
+ const clipMatch = code.match(clipRegex);
142
+ if (!clipMatch) {
143
+ return { hasFunctions: false, scopeCode: '', clipArea: '', normalFunctionCount: 0, quadraticFunctionCount: 0 };
144
+ }
145
+
146
+ const clipX1 = shouldRound ? roundToThreeDecimals(parseFloat(clipMatch[1])) : parseFloat(clipMatch[1]);
147
+ const clipY1 = shouldRound ? roundToThreeDecimals(parseFloat(clipMatch[2])) : parseFloat(clipMatch[2]);
148
+ const clipX2 = shouldRound ? roundToThreeDecimals(parseFloat(clipMatch[3])) : parseFloat(clipMatch[3]);
149
+ const clipY2 = shouldRound ? roundToThreeDecimals(parseFloat(clipMatch[4])) : parseFloat(clipMatch[4]);
150
+
151
+ const functionPlotRegex = /\\draw\[([^\]]*)\]\s*plot\s*\(\\x,\{([^}]*)\}\)/g;
152
+ const functionMatches = [...code.matchAll(functionPlotRegex)];
153
+ const functions = [];
154
+ const containsLnRegex = /ln\s*\(/i;
155
+
156
+ functionMatches.forEach(match => {
157
+ const options = match[1];
158
+ const functionExpression = match[2];
159
+ const domainRegex = /domain=([^:]+):([^,\]]+)/;
160
+ const domainMatch = options.match(domainRegex);
161
+
162
+ if (domainMatch) {
163
+ let domainStart = domainMatch[1], domainEnd = domainMatch[2];
164
+ if (containsLnRegex.test(functionExpression)) {
165
+ if (Math.abs(parseFloat(domainStart)) < 0.001) domainStart = '0.001';
166
+ }
167
+ if (shouldRound) {
168
+ const s = parseFloat(domainStart), e = parseFloat(domainEnd);
169
+ if (!isNaN(s)) domainStart = roundToThreeDecimals(s).toString();
170
+ if (!isNaN(e)) domainEnd = roundToThreeDecimals(e).toString();
171
+ }
172
+ let lineStyle = '';
173
+ if (options.includes('dash pattern=on 1pt off 1pt on 1pt off 4pt')) lineStyle = 'dash dot';
174
+ else if (options.includes('dash pattern=on 1pt off 1pt')) lineStyle = 'dashed';
175
+ else if (options.includes('dotted')) lineStyle = 'dotted';
176
+
177
+ let newOptions = 'smooth,samples=500';
178
+ if (lineStyle) newOptions = lineStyle + ',' + newOptions;
179
+
180
+ functions.push({ original: match[0], options: newOptions, domainStart, domainEnd, expression: functionExpression, lineStyle });
181
+ }
182
+ });
183
+
184
+ const quadraticResult = extractQuadraticFunctions(code, shouldRound);
185
+ let result = ' \\begin{scope}\n';
186
+ result += ` \\clip(${clipX1},${clipY1}) rectangle (${clipX2},${clipY2});\n`;
187
+ functions.forEach(func => {
188
+ result += ` \\draw[${func.options},domain=${func.domainStart}:${func.domainEnd}] plot(\\x,{${func.expression}});\n`;
189
+ });
190
+ if (quadraticResult.hasQuadratic) {
191
+ quadraticResult.quadraticFunctions.forEach(quad => {
192
+ result += ` \\draw[${quad.options}] plot(\\x,{${quad.expression}});\n`;
193
+ });
194
+ }
195
+ result += ' \\end{scope}\n';
196
+
197
+ return {
198
+ hasFunctions: functions.length > 0 || quadraticResult.hasQuadratic,
199
+ scopeCode: result,
200
+ clipArea: `(${clipX1},${clipY1}) rectangle (${clipX2},${clipY2})`,
201
+ normalFunctionCount: functions.length,
202
+ quadraticFunctionCount: quadraticResult.quadraticFunctions.length
203
+ };
204
+ } catch (error) {
205
+ return { hasFunctions: false, scopeCode: '', clipArea: '', normalFunctionCount: 0, quadraticFunctionCount: 0 };
206
+ }
207
+ }
208
+
209
+ function extractParametricPlots(code, shouldRound) {
210
+ try {
211
+ const parametricRegex = /\\draw\[([^\]]*)\]\s*plot\[parametric\]\s*function\{([^}]+)\};/g;
212
+ const matches = [...code.matchAll(parametricRegex)];
213
+ const plots = [];
214
+
215
+ matches.forEach(match => {
216
+ const options = match[1];
217
+ const functionStr = match[2];
218
+ const commaMatch = functionStr.match(/([^,]+),([^,]+)/);
219
+ if (!commaMatch) return;
220
+ let xExpr = commaMatch[1].trim(), yExpr = commaMatch[2].trim();
221
+
222
+ if (shouldRound) {
223
+ xExpr = xExpr.replace(/(\d+\.\d{4,})/g, m => roundToThreeDecimals(parseFloat(m)).toString());
224
+ yExpr = yExpr.replace(/(\d+\.\d{4,})/g, m => roundToThreeDecimals(parseFloat(m)).toString());
225
+ }
226
+ xExpr = xExpr.replace(/t\*\*\((\d+)\)/g, '\\t^$1');
227
+ xExpr = xExpr.replace(/\(1-t\)\*\*\((\d+)\)/g, '(1-\\t)^$1');
228
+ yExpr = yExpr.replace(/t\*\*\((\d+)\)/g, '\\t^$1');
229
+ yExpr = yExpr.replace(/\(1-t\)\*\*\((\d+)\)/g, '(1-\\t)^$1');
230
+ xExpr = xExpr.replace(/([ (+\-*/,])t(?!\w)/g, '$1\\t');
231
+ yExpr = yExpr.replace(/([ (+\-*/,])t(?!\w)/g, '$1\\t');
232
+ xExpr = xExpr.replace(/^t(?!\w)/, '\\t');
233
+ yExpr = yExpr.replace(/^t(?!\w)/, '\\t');
234
+
235
+ const lineStyle = convertLineStyle(options);
236
+ plots.push({ original: match[0], options, xExpression: xExpr, yExpression: yExpr, lineStyle });
237
+ });
238
+ return { hasParametricPlots: plots.length > 0, plots };
239
+ } catch (error) {
240
+ return { hasParametricPlots: false, plots: [] };
241
+ }
242
+ }
243
+
244
+ // ─── 主清洗函数 ──────────────────────────────────────────
245
+
246
+ function cleanTikZCode(code, opts = {}) {
247
+ code = code.replace(/\\draw\s+\[/g, '\\\\draw[');
248
+ const shouldRound = opts.round !== false;
249
+ const includePoints = opts.points !== false;
250
+ const includeLabels = opts.labels !== false;
251
+
252
+ const tikzMatch = code.match(/\\begin\{tikzpicture\}([\s\S]*?)\\end\{tikzpicture\}/);
253
+ if (!tikzMatch) throw new Error('未找到 tikzpicture 环境');
254
+
255
+ // 点
256
+ const pointRegex = /\\draw\s*\[fill=[^\]]+\]\s*\(([^)]+)\)\s*circle\s*\(([^)]+)\);/g;
257
+ const nodeRegex = /\\draw\[color=[^\]]+\]\s*\([^)]+\)\s*node\s*\{\$(?!.*\\textrm\{\\degre\})([^$]+(?:\{[^}]*\})?[^$]*)\$\}/g;
258
+ const pointMap = new Map();
259
+ const pointMatches = [...code.matchAll(pointRegex)];
260
+ const nodeMatches = [...code.matchAll(nodeRegex)];
261
+
262
+ // Modern GeoGebra exports LaTeX Text objects as anchored nodes. Parse each
263
+ // complete line so nested math braces (e.g. mathit{A}) are preserved.
264
+ const anchoredNodes = [...code.matchAll(/\\draw\s*(?:\[[^\]]*\])?\s*\(([^)]+)\)\s*node\s*\[([^\]]+)\]\s*\{([^\r\n]*)\};/g)];
265
+ const usedAnchoredNodes = new Set();
266
+ const allPoints = [];
267
+ pointMatches.forEach(match => {
268
+ const coords = match[1];
269
+ const formattedCoords = formatCoordinate(`(${coords})`, shouldRound);
270
+ allPoints.push({ original: coords, formatted: formattedCoords.slice(1, -1) });
271
+ });
272
+ allPoints.forEach((point, index) => {
273
+ const [x, y] = point.original.split(',').map(Number);
274
+ const candidates = anchoredNodes.map((node, i) => {
275
+ const label = node[3].match(/^\$\\mathit\{([A-Za-z][A-Za-z0-9_]*)\}\$$/)?.[1]
276
+ || node[3].match(/^\$([A-Za-z][A-Za-z0-9_]*)\$$/)?.[1];
277
+ const [nx, ny] = node[1].split(',').map(Number);
278
+ return { i, label, distance: Math.hypot(nx-x, ny-y) };
279
+ }).filter(n => n.label && !usedAnchoredNodes.has(n.i)).sort((a,b) => a.distance-b.distance);
280
+ if (candidates.length && candidates[0].distance < 1) {
281
+ pointMap.set(candidates[0].label, point.formatted);
282
+ usedAnchoredNodes.add(candidates[0].i);
283
+ } else if (index < nodeMatches.length) {
284
+ pointMap.set(nodeMatches[index][1], point.formatted);
285
+ } else {
286
+ // Unlabelled points still need their visible marker.
287
+ pointMap.set(`gikzPoint${index}`, point.formatted);
288
+ }
289
+ });
290
+
291
+ // 函数
292
+ const functionPlotsResult = extractFunctionPlots(code, shouldRound);
293
+ // 贝塞尔
294
+ const parametricPlotResult = extractParametricPlots(code, shouldRound);
295
+
296
+ // 文本标签
297
+ const textLabels = anchoredNodes.filter((_, i) => !usedAnchoredNodes.has(i)).map(match => ({
298
+ coords: formatCoordinate(`(${match[1]})`, shouldRound), options: match[2], content: match[3]
299
+ }));
300
+
301
+ // 角度标签
302
+ const angleLabelRegex = /\\draw\[color=([^\]]+)\]\s*\(([^)]+)\)\s*node\s*\{\$([^}]+)\\textrm\{\\degre\}\$\};/g;
303
+ const angleLabelMatches = [...code.matchAll(angleLabelRegex)];
304
+ const angleLabels = [];
305
+ angleLabelMatches.forEach(match => {
306
+ angleLabels.push({
307
+ color: match[1],
308
+ coords: formatCoordinate(`(${match[2]})`, shouldRound),
309
+ content: match[3]
310
+ });
311
+ });
312
+
313
+ // 线段
314
+ const lineRegex = /\\draw\s*(\[[^\]]+\])?\s*\(([^)]+)\)\s*--\s*\(([^)]+)\);/g;
315
+ const lineMatches = [...code.matchAll(lineRegex)];
316
+
317
+ // 圆
318
+ const circleRegex = /\\draw\s*\[([^\]]*line[^\]]*)\]\s*\(([^)]+)\)\s*circle\s*\(([^)]+)\);/g;
319
+ const circleMatches = [...code.matchAll(circleRegex)];
320
+ const circles = [];
321
+ circleMatches.forEach(match => {
322
+ const options = match[1];
323
+ const centerCoords = match[2];
324
+ const radius = match[3];
325
+ const formattedCenter = formatCoordinate(`(${centerCoords})`, shouldRound);
326
+ const lineStyle = convertLineStyle(options);
327
+ let centerLabel = null;
328
+ for (const [label, coords] of pointMap) {
329
+ const coordStr = coords.includes('(') ? coords : `(${coords})`;
330
+ if (coordStr.includes(centerCoords.trim()) || formattedCenter.includes(coords)) {
331
+ centerLabel = label; break;
332
+ }
333
+ }
334
+ circles.push({
335
+ center: centerLabel ? `(${centerLabel})` : formattedCenter,
336
+ radius: shouldRound ? roundToThreeDecimals(parseFloat(radius)) + (radius.includes('cm') ? 'cm' : '') : radius,
337
+ lineStyle
338
+ });
339
+ });
340
+
341
+ // 圆弧
342
+ const arcRegex = /\\draw\s*(\[[^\]]*\])?\s*plot\[domain=([^:]+):([^,]+),variable=\\t\]\(\{1\*([^\*]+)\*cos\(\\t r\)\+0\*[^\*]+\*sin\(\\t r\)\},\{0\*[^\*]+\*cos\(\\t r\)\+1\*[^\*]+\*sin\(\\t r\)\}\);/g;
343
+ const arcMatches = [...code.matchAll(arcRegex)];
344
+ const arcs = [];
345
+ arcMatches.forEach(match => {
346
+ const style = match[1] || '';
347
+ const rv = (val) => shouldRound ? roundToThreeDecimals(parseFloat(val)) : val;
348
+
349
+ let lineStyle = '';
350
+ if (style.includes('dash pattern=on 1pt off 1pt on 1pt off 4pt')) lineStyle = 'dash dot';
351
+ else if (style.includes('dash pattern=on 1pt off 1pt')) lineStyle = 'dashed';
352
+ else if (style.includes('dotted')) lineStyle = 'dotted';
353
+
354
+ const shiftRegex = /shift=\{\(([^,]+),([^)]+)\)\}/;
355
+ const shiftMatch = style.match(shiftRegex);
356
+ let styleParts = [];
357
+ if (lineStyle) styleParts.push(lineStyle);
358
+ if (shiftMatch) styleParts.push(`shift={(${rv(shiftMatch[1])},${rv(shiftMatch[2])})}`);
359
+ styleParts.push('smooth');
360
+ let newStyle = styleParts.length > 0 ? '[' + styleParts.join(', ') + ']' : '';
361
+
362
+ arcs.push({ style: newStyle, startAngle: rv(match[2]), endAngle: rv(match[3]), radius: rv(match[4]) });
363
+ });
364
+
365
+ // 扇形
366
+ const sectorRegex = /\\draw\s*\[shift=\{\(([^,]+),([^)]+)\)\}[^\]]*\]\s*\(0,0\)\s*--\s*plot\[domain=([^:]+):([^,\]]+),variable=\\t\]\(\{1\*([^\*]+)\*cos\(\\t r\)\+0\*[^\*]+\*sin\(\\t r\)\},\{0\*[^\*]+\*cos\(\\t r\)\+1\*[^\*]+\*sin\(\\t r\)\}\)\s*--\s*cycle\s*;/g;
367
+ const sectorMatches = [...code.matchAll(sectorRegex)];
368
+ const sectors = [];
369
+ sectorMatches.forEach(match => {
370
+ const rv = (val) => shouldRound ? roundToThreeDecimals(parseFloat(val)) : val;
371
+ const style = match[0].match(/\\draw\s*\[([^\]]+)\]/)[1];
372
+ sectors.push({
373
+ shiftX: rv(match[1].trim()), shiftY: rv(match[2].trim()),
374
+ startAngle: rv(match[3].trim()), endAngle: rv(match[4].trim()),
375
+ radius: rv(match[5].trim()), lineStyle: convertLineStyle(style)
376
+ });
377
+ });
378
+
379
+ // 椭圆
380
+ const ellipseRegex = /\\draw\s*\[([^\]]*)\]\s*\(([^)]+)\)\s*ellipse\s*\(([^)]+)\);/g;
381
+ const ellipseMatches = [...code.matchAll(ellipseRegex)];
382
+ const ellipses = [];
383
+ ellipseMatches.forEach(match => {
384
+ const options = match[1], centerCoords = match[2], radii = match[3];
385
+ const radiusParts = radii.split(' and ');
386
+ let xR = radiusParts[0]?.trim() || '', yR = radiusParts[1]?.trim() || '';
387
+ const formattedCenter = formatCoordinate(`(${centerCoords})`, shouldRound);
388
+ let centerLabel = null;
389
+ for (const [label, coords] of pointMap) {
390
+ const cs = coords.includes('(') ? coords : `(${coords})`;
391
+ if (cs.includes(centerCoords.trim()) || formattedCenter.includes(coords)) { centerLabel = label; break; }
392
+ }
393
+ let rotationAngle = 0, rotationCenter = null;
394
+ const rotateMatch = options.match(/rotate around=\{([^:]+):\(([^)]+)\)\}/);
395
+ if (rotateMatch) {
396
+ rotationAngle = parseFloat(rotateMatch[1]);
397
+ if (shouldRound) rotationAngle = roundToThreeDecimals(rotationAngle);
398
+ const rc = rotateMatch[2];
399
+ const frc = formatCoordinate(`(${rc})`, shouldRound);
400
+ for (const [label, coords] of pointMap) {
401
+ const cs = coords.includes('(') ? coords : `(${coords})`;
402
+ if (cs.includes(rc.trim()) || frc.includes(coords)) { rotationCenter = label; break; }
403
+ }
404
+ if (!rotationCenter) rotationCenter = frc;
405
+ }
406
+ const lineStyle = convertLineStyle(options);
407
+ if (shouldRound && xR && yR) {
408
+ const xU = xR.replace(/[0-9.\-\s]/g, ''), yU = yR.replace(/[0-9.\-\s]/g, '');
409
+ xR = roundToThreeDecimals(parseFloat(xR)) + xU;
410
+ yR = roundToThreeDecimals(parseFloat(yR)) + yU;
411
+ }
412
+ const obj = { center: centerLabel ? `(${centerLabel})` : formattedCenter, xRadius: xR, yRadius: yR, lineStyle, hasRotation: !!rotateMatch };
413
+ if (rotateMatch) { obj.rotationAngle = rotationAngle; obj.rotationCenter = rotationCenter ? `(${rotationCenter})` : formattedCenter; }
414
+ ellipses.push(obj);
415
+ });
416
+
417
+ // 角度标记
418
+ const angleRegex = /\\draw\s*\[shift=\{\(([^,]+),([^)]+)\)\}[^\]]*\]\s*\(0,0\)\s*--\s*\(([^:]+):([^)]+)\)\s*arc\s*\(([^:]+):([^)]+):([^)]+)\)\s*--\s*cycle;/g;
419
+ const angleMatches = [...code.matchAll(angleRegex)];
420
+ const angles = [];
421
+ angleMatches.forEach(match => {
422
+ const cX = match[1].trim(), cY = match[2].trim();
423
+ const formattedCenter = formatCoordinate(`(${cX},${cY})`, shouldRound);
424
+ let centerLabel = null;
425
+ for (const [label, coords] of pointMap) {
426
+ const cs = coords.includes('(') ? coords : `(${coords})`;
427
+ if (cs.includes(`${cX},${cY}`) || formattedCenter.includes(coords)) { centerLabel = label; break; }
428
+ }
429
+ const rv = (v) => shouldRound ? roundToThreeDecimals(parseFloat(v)) : v;
430
+ angles.push({
431
+ center: centerLabel ? `(${centerLabel})` : formattedCenter,
432
+ startAngle: rv(match[3].trim()), endAngle: rv(match[6].trim()), radius: rv(match[4].trim())
433
+ });
434
+ });
435
+
436
+ // ─── 组装输出 ────────────────────────────────────────
437
+ let result = '\\begin{tikzpicture}[scale=1]\n';
438
+
439
+ if (pointMap.size > 0) {
440
+ result += ' % 坐标点定义\n';
441
+ for (const [label, coords] of pointMap) {
442
+ result += ` \\coordinate (${label}) at ${coords.includes('(') ? coords : `(${coords})`};\n`;
443
+ }
444
+ result += '\n';
445
+ }
446
+
447
+ if (functionPlotsResult.hasFunctions) {
448
+ result += ' % 函数图像\n' + functionPlotsResult.scopeCode + '\n';
449
+ }
450
+
451
+ if (parametricPlotResult.hasParametricPlots) {
452
+ result += ' % 贝塞尔曲线\n';
453
+ parametricPlotResult.plots.forEach(plot => {
454
+ const ls = plot.lineStyle ? `[${plot.lineStyle}, smooth, samples=100, domain=0:1, variable=\\t]` : '[smooth, samples=100, domain=0:1, variable=\\t]';
455
+ result += ` \\draw${ls} plot\n ({${plot.xExpression}},\n {${plot.yExpression}});\n\n`;
456
+ });
457
+ }
458
+
459
+ if (lineMatches.length > 0 || circles.length > 0 || arcs.length > 0 || ellipses.length > 0 || angles.length > 0 || angleLabels.length > 0 || sectors.length > 0) {
460
+ result += ' % 几何元素\n';
461
+
462
+ if (angles.length > 0) {
463
+ result += ' % 角度\n';
464
+ angles.forEach(a => {
465
+ result += ` \\fill [shift=${a.center}, gray!30] (0,0) -- (${a.startAngle}:${a.radius}) arc (${a.startAngle}:${a.endAngle}:${a.radius}) -- cycle;\n`;
466
+ });
467
+ result += '\n';
468
+ }
469
+
470
+ if (sectors.length > 0) {
471
+ result += ' % 扇形绘制\n';
472
+ sectors.forEach(s => {
473
+ const st = s.lineStyle ? `[${s.lineStyle}]` : '';
474
+ result += ` \\draw${st} [shift={(${s.shiftX},${s.shiftY})}] (0,0) -- plot[domain=${s.startAngle}:${s.endAngle}]({1*${s.radius}*cos(\\x r)+0*${s.radius}*sin(\\x r)},{0*${s.radius}*cos(\\x r)+1*${s.radius}*sin(\\x r)}) -- cycle;\n`;
475
+ });
476
+ if (lineMatches.length > 0) result += '\n';
477
+ }
478
+
479
+ if (circles.length > 0) {
480
+ circles.forEach(c => {
481
+ result += c.lineStyle
482
+ ? ` \\draw[${c.lineStyle}] ${c.center} circle (${c.radius});\n`
483
+ : ` \\draw ${c.center} circle (${c.radius});\n`;
484
+ });
485
+ if (lineMatches.length > 0 || arcs.length > 0) result += '\n';
486
+ }
487
+
488
+ if (ellipses.length > 0) {
489
+ result += ' % 椭圆\n';
490
+ ellipses.forEach(e => {
491
+ const drawOpts = [];
492
+ if (e.lineStyle) drawOpts.push(e.lineStyle);
493
+ if (e.hasRotation) drawOpts.push(`rotate around={${e.rotationAngle}:${e.rotationCenter}}`);
494
+ result += drawOpts.length > 0
495
+ ? ` \\draw[${drawOpts.join(', ')}] ${e.center} ellipse (${e.xRadius} and ${e.yRadius});\n`
496
+ : ` \\draw ${e.center} ellipse (${e.xRadius} and ${e.yRadius});\n`;
497
+ });
498
+ if (lineMatches.length > 0 || arcs.length > 0) result += '\n';
499
+ }
500
+
501
+ if (arcs.length > 0) {
502
+ result += ' % 圆弧绘制\n';
503
+ arcs.forEach(arc => {
504
+ const s = arc.style.trim() ? arc.style : '';
505
+ result += ` \\draw${s} plot[domain=${arc.startAngle}:${arc.endAngle}]({1*${arc.radius}*cos(\\x r)+0*${arc.radius}*sin(\\x r)},{0*${arc.radius}*cos(\\x r)+1*${arc.radius}*sin(\\x r)});\n`;
506
+ });
507
+ if (lineMatches.length > 0) result += '\n';
508
+ }
509
+
510
+ const processedLines = new Set();
511
+ const parseCoords = (str) => {
512
+ const m = str.match(/\(([^,]+),([^)]+)\)/);
513
+ return m ? { x: parseFloat(m[1]), y: parseFloat(m[2]) } : null;
514
+ };
515
+
516
+ lineMatches.forEach(match => {
517
+ const style = match[1] || '';
518
+ const coord1 = match[2], coord2 = match[3];
519
+ const convertedStyle = convertLineStyle(style);
520
+ let label1 = null, label2 = null;
521
+ const tc1 = formatCoordinate(`(${coord1})`, shouldRound);
522
+ const tc2 = formatCoordinate(`(${coord2})`, shouldRound);
523
+ const t1 = parseCoords(tc1), t2 = parseCoords(tc2);
524
+
525
+ for (const [label, coords] of pointMap) {
526
+ const pcs = coords.includes('(') ? coords : `(${coords})`;
527
+ const p = parseCoords(pcs);
528
+ if (p && t1 && Math.abs(p.x - t1.x) < 0.001 && Math.abs(p.y - t1.y) < 0.001) label1 = label;
529
+ if (p && t2 && Math.abs(p.x - t2.x) < 0.001 && Math.abs(p.y - t2.y) < 0.001) label2 = label;
530
+ }
531
+
532
+ if (label1 && label2) {
533
+ const key = `${label1}-${label2}`, rev = `${label2}-${label1}`;
534
+ if (!processedLines.has(key) && !processedLines.has(rev)) {
535
+ result += convertedStyle ? ` \\draw[${convertedStyle}] (${label1}) -- (${label2});\n` : ` \\draw (${label1}) -- (${label2});\n`;
536
+ processedLines.add(key);
537
+ }
538
+ } else {
539
+ const fc1 = formatCoordinate(`(${coord1})`, shouldRound);
540
+ const fc2 = formatCoordinate(`(${coord2})`, shouldRound);
541
+ result += convertedStyle ? ` \\draw[${convertedStyle}] ${fc1} -- ${fc2};\n` : ` \\draw ${fc1} -- ${fc2};\n`;
542
+ }
543
+ });
544
+ }
545
+
546
+ if (includePoints && pointMap.size > 0) {
547
+ result += '\n % 点标记\n';
548
+ for (const [label] of pointMap) {
549
+ result += ` \\draw[fill=black] (${label}) circle (1pt);\n`;
550
+ }
551
+ }
552
+
553
+ if (includeLabels && pointMap.size > 0) {
554
+ result += '\n % 点标签\n';
555
+ const allPointsData = [];
556
+ for (const [label, coords] of pointMap) {
557
+ const m = coords.match(/\(?([^,]+),([^)]+)\)?/);
558
+ if (m) allPointsData.push({ label, x: parseFloat(m[1]), y: parseFloat(m[2]) });
559
+ }
560
+ for (const point of allPointsData) {
561
+ if (/^gikzPoint\d+$/.test(point.label)) continue;
562
+ const pos = getSmartLabelPosition(point.x, point.y, allPointsData);
563
+ result += ` \\node [${pos}] at (${point.label}) {$${point.label}$};\n`;
564
+ }
565
+ }
566
+
567
+ if (angleLabels.length > 0) {
568
+ result += '\n % 角度标签\n';
569
+ angleLabels.forEach(l => { result += ` \\draw ${l.coords} node {$${l.content}^{\\circ}$};\n`; });
570
+ }
571
+
572
+ if (textLabels.length > 0) {
573
+ result += '\n % 文本标签\n';
574
+ textLabels.forEach(l => { result += ` \\draw ${l.coords} node[${l.options}] {${l.content}};\n`; });
575
+ }
576
+
577
+ result += '\\end{tikzpicture}';
578
+ return result;
579
+ }
580
+
581
+ // ─── GeoGebra XML/GGB 转换 ──────────────────────────────
582
+
583
+ function convertGeoGebraXML(xmlStr, opts = {}) {
584
+ const shouldRound = opts.round !== false;
585
+ const includePoints = opts.points !== false;
586
+ const includeLabels = opts.labels !== false;
587
+ const rv = v => shouldRound ? roundToThreeDecimals(v) : v;
588
+ const de = s => s.replace(/&amp;/g, '&').replace(/&lt;/g, '<')
589
+ .replace(/&gt;/g, '>').replace(/&apos;/g, "'").replace(/&quot;/g, '"');
590
+
591
+ const constrM = xmlStr.match(/<construction[^>]*>([\s\S]*?)<\/construction>/);
592
+ if (!constrM) throw new Error('XML 中未找到 construction 块');
593
+ const constr = constrM[1];
594
+
595
+ // 解析元素
596
+ const elements = new Map();
597
+ const elRe = /<element\s+type="([^"]+)"\s+label="([^"]+)">([\s\S]*?)<\/element>/g;
598
+ let m;
599
+ while ((m = elRe.exec(constr))) {
600
+ const label = de(m[2]), body = m[3];
601
+ const el = { type: m[1], label };
602
+
603
+ const showM = body.match(/<show\s+object="([^"]+)"\s+label="([^"]+)"/);
604
+ el.visible = showM ? showM[1] === 'true' : false;
605
+ el.showLabel = showM ? showM[2] === 'true' : false;
606
+
607
+ const coordsM = body.match(/<coords\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/);
608
+ if (coordsM) {
609
+ const z = parseFloat(coordsM[3]) || 1;
610
+ el.x = parseFloat(coordsM[1]) / z;
611
+ el.y = parseFloat(coordsM[2]) / z;
612
+ }
613
+
614
+ const valM = body.match(/<value\s+val="([^"]+)"/);
615
+ if (valM) el.value = parseFloat(valM[1]);
616
+ const arcM = body.match(/<arcSize\s+val="([^"]+)"/);
617
+ if (arcM) el.arcSize = parseInt(arcM[1]);
618
+
619
+ const colM = body.match(/<objColor[^>]+alpha="([^"]+)"/);
620
+ if (colM) el.alpha = parseFloat(colM[1]);
621
+
622
+ const lineM = body.match(/<lineStyle\s+thickness="([^"]+)"\s+type="([^"]+)"/);
623
+ if (lineM) { el.lineThickness = parseInt(lineM[1]); el.lineType = parseInt(lineM[2]); }
624
+
625
+ if (el.x !== undefined && (isNaN(el.x) || isNaN(el.y))) continue;
626
+ elements.set(label, el);
627
+ }
628
+
629
+ // 解析命令
630
+ const commands = [];
631
+ const cmdRe = /<command\s+name="([^"]+)">([\s\S]*?)<\/command>/g;
632
+ while ((m = cmdRe.exec(constr))) {
633
+ const cmd = { name: m[1], inputs: [], outputs: [] };
634
+ const inM = m[2].match(/<input\s+([^/]*)\/>/);
635
+ if (inM) { const r = /a(\d+)="([^"]+)"/g; let a; while ((a = r.exec(inM[1]))) cmd.inputs[+a[1]] = de(a[2]); }
636
+ const outM = m[2].match(/<output\s+([^/]*)\/>/);
637
+ if (outM) { const r = /a(\d+)="([^"]+)"/g; let a; while ((a = r.exec(outM[1]))) cmd.outputs[+a[1]] = de(a[2]); }
638
+ commands.push(cmd);
639
+ }
640
+
641
+ const cmdByOut = new Map();
642
+ for (const cmd of commands)
643
+ for (const lbl of cmd.outputs) if (lbl) cmdByOut.set(lbl, cmd);
644
+
645
+ // 多边形边标签(排除独立绘制)
646
+ const polyEdges = new Set();
647
+ for (const cmd of commands) {
648
+ if (cmd.name === 'Polygon')
649
+ for (let i = 1; i < cmd.outputs.length; i++) if (cmd.outputs[i]) polyEdges.add(cmd.outputs[i]);
650
+ }
651
+
652
+ // 收集可见元素
653
+ const refPts = new Set();
654
+ const lsName = t => { switch(t){ case 10: case 15: return 'dashed'; case 20: return 'dotted'; case 30: return 'dash dot'; default: return ''; } };
655
+
656
+ // 线段
657
+ const segs = [];
658
+ for (const [label, el] of elements) {
659
+ if (!el.visible || el.type !== 'segment' || polyEdges.has(label)) continue;
660
+ const cmd = cmdByOut.get(label);
661
+ if (cmd && cmd.name === 'Segment' && cmd.inputs[0] && cmd.inputs[1]) {
662
+ segs.push({ from: cmd.inputs[0], to: cmd.inputs[1], ls: lsName(el.lineType || 0) });
663
+ refPts.add(cmd.inputs[0]); refPts.add(cmd.inputs[1]);
664
+ }
665
+ }
666
+
667
+ // 圆
668
+ const circs = [];
669
+ for (const [label, el] of elements) {
670
+ if (!el.visible || el.type !== 'conic') continue;
671
+ const cmd = cmdByOut.get(label);
672
+ if (cmd && cmd.name === 'Circle') {
673
+ const cEl = elements.get(cmd.inputs[0]), tEl = elements.get(cmd.inputs[1]);
674
+ if (cEl && tEl && cEl.x !== undefined && tEl.x !== undefined) {
675
+ const dx = tEl.x - cEl.x, dy = tEl.y - cEl.y;
676
+ circs.push({ center: cmd.inputs[0], radius: rv(Math.sqrt(dx * dx + dy * dy)) });
677
+ refPts.add(cmd.inputs[0]);
678
+ }
679
+ }
680
+ }
681
+
682
+ // 多边形
683
+ const polys = [];
684
+ for (const [label, el] of elements) {
685
+ if (!el.visible || el.type !== 'polygon') continue;
686
+ const cmd = cmdByOut.get(label);
687
+ if (cmd && cmd.name === 'Polygon') {
688
+ const verts = [];
689
+ for (let i = 0; cmd.inputs[i]; i++) { verts.push(cmd.inputs[i]); refPts.add(cmd.inputs[i]); }
690
+ polys.push({ vertices: verts, alpha: el.alpha || 0 });
691
+ }
692
+ }
693
+
694
+ // 角度
695
+ const angs = [];
696
+ for (const [label, el] of elements) {
697
+ if (!el.visible || el.type !== 'angle') continue;
698
+ const cmd = cmdByOut.get(label);
699
+ if (cmd && cmd.name === 'Angle') {
700
+ angs.push({ ptA: cmd.inputs[0], vertex: cmd.inputs[1], ptC: cmd.inputs[2], value: el.value });
701
+ refPts.add(cmd.inputs[0]); refPts.add(cmd.inputs[1]); refPts.add(cmd.inputs[2]);
702
+ }
703
+ }
704
+
705
+ for (const [label, el] of elements)
706
+ if (el.type === 'point' && (el.visible || el.showLabel)) refPts.add(label);
707
+
708
+ const coords = [];
709
+ for (const label of refPts) {
710
+ const el = elements.get(label);
711
+ if (el && el.type === 'point' && el.x !== undefined) coords.push({ label, x: rv(el.x), y: rv(el.y) });
712
+ }
713
+ coords.sort((a, b) => a.label.localeCompare(b.label));
714
+
715
+ // 生成 TikZ
716
+ let out = '\\begin{tikzpicture}[scale=1]\n';
717
+
718
+ if (coords.length > 0) {
719
+ out += ' % 坐标点定义\n';
720
+ for (const c of coords) out += ` \\coordinate (${c.label}) at (${c.x},${c.y});\n`;
721
+ out += '\n';
722
+ }
723
+
724
+ out += ' % 几何元素\n';
725
+
726
+ if (angs.length > 0) {
727
+ for (const ang of angs) {
728
+ const vEl = elements.get(ang.vertex), aEl = elements.get(ang.ptA), cEl = elements.get(ang.ptC);
729
+ if (!vEl || !aEl || !cEl) continue;
730
+ let sa = rv(Math.atan2(aEl.y - vEl.y, aEl.x - vEl.x) * 180 / Math.PI);
731
+ let ea = rv(Math.atan2(cEl.y - vEl.y, cEl.x - vEl.x) * 180 / Math.PI);
732
+ if (ea <= sa) ea += 360;
733
+ out += ` \\fill[green!10] (${ang.vertex}) -- ++(${sa}:0.4) arc (${sa}:${ea}:0.4) -- cycle;\n`;
734
+ }
735
+ out += '\n';
736
+ }
737
+
738
+ if (polys.length > 0) {
739
+ for (const p of polys) {
740
+ if (p.alpha > 0) {
741
+ const pth = p.vertices.map(v => `(${v})`).join(' -- ') + ' -- cycle';
742
+ out += ` \\fill[gray!20, fill opacity=${rv(p.alpha)}] ${pth};\n`;
743
+ }
744
+ }
745
+ out += '\n';
746
+ }
747
+
748
+ if (circs.length > 0) {
749
+ for (const c of circs) out += ` \\draw (${c.center}) circle (${c.radius});\n`;
750
+ out += '\n';
751
+ }
752
+
753
+ if (segs.length > 0) {
754
+ const drawn = new Set();
755
+ for (const s of segs) {
756
+ const k = [s.from, s.to].sort().join('|');
757
+ if (drawn.has(k)) continue;
758
+ drawn.add(k);
759
+ out += s.ls ? ` \\draw[${s.ls}] (${s.from}) -- (${s.to});\n` : ` \\draw (${s.from}) -- (${s.to});\n`;
760
+ }
761
+ out += '\n';
762
+ }
763
+
764
+ if (includePoints) {
765
+ const visPts = coords.filter(c => { const e = elements.get(c.label); return e && e.visible; });
766
+ if (visPts.length > 0) {
767
+ out += ' % 点标记\n';
768
+ for (const p of visPts) out += ` \\draw[fill=black] (${p.label}) circle (1pt);\n`;
769
+ out += '\n';
770
+ }
771
+ }
772
+
773
+ if (includeLabels) {
774
+ const lblPts = coords.filter(c => { const e = elements.get(c.label); return e && (e.visible || e.showLabel); });
775
+ if (lblPts.length > 0) {
776
+ out += ' % 点标签\n';
777
+ for (const p of lblPts) {
778
+ const pos = getSmartLabelPosition(p.x, p.y, coords);
779
+ out += ` \\node[${pos}] at (${p.label}) {$${p.label}$};\n`;
780
+ }
781
+ }
782
+ }
783
+
784
+ out += '\\end{tikzpicture}';
785
+ return out;
786
+ }
787
+
788
+ // ─── 包装函数:standalone / tikzonly ────────────────────
789
+
790
+ function wrapStandalone(tikzCode) {
791
+ return `\\documentclass[border=5pt]{standalone}
792
+ \\usepackage{tikz}
793
+ \\usepackage{pgfplots}
794
+ \\pgfplotsset{compat=1.15}
795
+ \\usetikzlibrary{arrows.meta}
796
+
797
+ \\begin{document}
798
+ ${tikzCode}
799
+ \\end{document}
800
+ `;
801
+ }
802
+
803
+ // ─── CLI ─────────────────────────────────────────────────
804
+
805
+
806
+ module.exports = { cleanTikZCode, wrapStandalone, convertGeoGebraXML };