@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
@@ -0,0 +1,1712 @@
1
+ // @ts-nocheck
2
+ import puppeteer, { Browser, Page } from 'puppeteer-core';
3
+ import { resolveBrowser } from '../studio/browser';
4
+ import { constructionHtml } from '../export/html';
5
+ import { currentWorkspace } from '../export/workspace';
6
+ import {
7
+ GeoGebraConfig,
8
+ GeoGebraAPI,
9
+ GeoGebraCommandResult,
10
+ GeoGebraObject,
11
+ GeoGebraError,
12
+ GeoGebraConnectionError,
13
+ GeoGebraCommandError
14
+ } from '../types/geogebra';
15
+ import logger from './logger';
16
+ import { randomUUID as uuidv4 } from 'crypto';
17
+
18
+ /**
19
+ * GeoGebra Instance - Manages a single GeoGebra instance via Puppeteer
20
+ */
21
+ /**
22
+ * Escapes a string for safe use in JavaScript code.
23
+ * Replaces backslashes and single quotes with their escaped equivalents.
24
+ */
25
+ function escapeForJavaScript(input: string): string {
26
+ return input.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
27
+ }
28
+
29
+ export class GeoGebraInstance implements GeoGebraAPI {
30
+ private browser?: Browser | undefined;
31
+ private page?: Page | undefined;
32
+ private isInitialized: boolean = false;
33
+ private lastActivity: Date = new Date();
34
+
35
+ public readonly id: string;
36
+ public readonly config: GeoGebraConfig;
37
+
38
+ constructor(config: GeoGebraConfig = {}) {
39
+ this.id = uuidv4();
40
+ this.config = {
41
+ appName: 'classic', // Changed from 'graphing' to 'classic' for full functionality
42
+ width: 800,
43
+ height: 600,
44
+ showMenuBar: false,
45
+ showToolBar: false,
46
+ showAlgebraInput: false,
47
+ showResetIcon: false,
48
+ enableRightClick: true,
49
+ language: 'en',
50
+ ...config
51
+ };
52
+
53
+ logger.info(`Created GeoGebra instance ${this.id}`, { config: this.config });
54
+ }
55
+
56
+ /**
57
+ * Initialize the GeoGebra instance
58
+ */
59
+ async initialize(headless: boolean = true, browserArgs: string[] = []): Promise<void> {
60
+ try {
61
+ logger.info(`Initializing GeoGebra instance ${this.id}`, { headless });
62
+
63
+ // Launch browser
64
+ this.browser = await puppeteer.launch({
65
+ headless,
66
+ executablePath: resolveBrowser(),
67
+ args: [
68
+ '--no-sandbox',
69
+ '--disable-setuid-sandbox',
70
+ '--disable-dev-shm-usage',
71
+ '--disable-accelerated-2d-canvas',
72
+ '--no-first-run',
73
+ '--no-zygote',
74
+ '--disable-gpu',
75
+ ...browserArgs
76
+ ]
77
+ });
78
+
79
+ // Create new page
80
+ this.page = await this.browser.newPage();
81
+
82
+ // Set viewport
83
+ await this.page.setViewport({
84
+ width: this.config.width || 800,
85
+ height: this.config.height || 600
86
+ });
87
+
88
+ // Load GeoGebra
89
+ await this.loadGeoGebra();
90
+
91
+ // Wait for GeoGebra to be ready
92
+ await this.waitForReady();
93
+
94
+ this.isInitialized = true;
95
+ this.updateActivity();
96
+
97
+ logger.info(`GeoGebra instance ${this.id} initialized successfully`);
98
+ } catch (error) {
99
+ logger.error(`Failed to initialize GeoGebra instance ${this.id}`, error);
100
+ await this.cleanup();
101
+ throw new GeoGebraConnectionError(
102
+ `Failed to initialize GeoGebra: ${error instanceof Error ? error.message : String(error)}`
103
+ );
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Load GeoGebra applet in the browser
109
+ */
110
+ private async loadGeoGebra(): Promise<void> {
111
+ if (!this.page) {
112
+ throw new GeoGebraConnectionError('Page not initialized');
113
+ }
114
+
115
+ const appletHTML = this.generateAppletHTML();
116
+
117
+ await this.page.setContent(appletHTML);
118
+
119
+ // Wait for GeoGebra to load
120
+ await this.page.waitForFunction('window.ggbReady || window.ggbError', { timeout: 60000 });
121
+ const bootError = await this.page.evaluate(() => (window as any).ggbError);
122
+ if (bootError) throw new GeoGebraConnectionError(bootError);
123
+ }
124
+
125
+ /**
126
+ * Generate HTML content with GeoGebra applet
127
+ */
128
+ private generateAppletHTML(): string {
129
+ return constructionHtml({ width: this.config.width, height: this.config.height, ggbBase64: this.config.ggbBase64 });
130
+ }
131
+
132
+ /**
133
+ * Wait for GeoGebra to be ready
134
+ */
135
+ private async waitForReady(timeout: number = 30000): Promise<void> {
136
+ if (!this.page) {
137
+ throw new GeoGebraConnectionError('Page not initialized');
138
+ }
139
+
140
+ try {
141
+ // Wait for both ggbReady flag and ggbApplet to be available
142
+ await this.page.waitForFunction(
143
+ 'window.ggbReady === true && window.ggbApplet && typeof window.ggbApplet.evalCommand === "function"',
144
+ { timeout }
145
+ );
146
+
147
+ // Additional verification that the applet is functional
148
+ const isWorking = await this.page.evaluate(() => {
149
+ try {
150
+ return typeof window.ggbApplet.evalCommand === 'function' &&
151
+ typeof window.ggbApplet.exists === 'function';
152
+ } catch (e) {
153
+ return false;
154
+ }
155
+ });
156
+
157
+ if (!isWorking) {
158
+ throw new Error('GeoGebra applet methods not available');
159
+ }
160
+
161
+ logger.debug(`GeoGebra instance ${this.id} is ready and functional`);
162
+ } catch (error) {
163
+ // Log the current state for debugging
164
+ try {
165
+ const debugInfo = await this.page.evaluate(() => ({
166
+ ggbReady: (window as any).ggbReady,
167
+ ggbAppletExists: !!(window as any).ggbApplet,
168
+ ggbAppletType: typeof (window as any).ggbApplet,
169
+ evalCommandExists: (window as any).ggbApplet && typeof (window as any).ggbApplet.evalCommand,
170
+ pageLocation: window.location.href,
171
+ pageTitle: document.title
172
+ }));
173
+ logger.error(`GeoGebra initialization failed. Debug info:`, debugInfo);
174
+ } catch (debugError) {
175
+ logger.error(`Failed to get debug info during initialization failure`, debugError);
176
+ }
177
+
178
+ throw new GeoGebraConnectionError('GeoGebra failed to initialize within timeout');
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Execute a GeoGebra command
184
+ */
185
+ async evalCommand(command: string): Promise<GeoGebraCommandResult> {
186
+ this.ensureInitialized();
187
+ this.updateActivity();
188
+
189
+ try {
190
+ logger.debug(`Executing command on instance ${this.id}: ${command}`);
191
+
192
+ // First verify that the applet is still available
193
+ const appletCheck = await this.page!.evaluate(() => {
194
+ return {
195
+ appletExists: !!(window as any).ggbApplet,
196
+ evalCommandExists: !!(window as any).ggbApplet && typeof (window as any).ggbApplet.evalCommand === 'function'
197
+ };
198
+ });
199
+
200
+ if (!appletCheck.appletExists || !appletCheck.evalCommandExists) {
201
+ throw new Error(`GeoGebra applet not available: ${JSON.stringify(appletCheck)}`);
202
+ }
203
+
204
+ const result = await this.page!.evaluate((cmd) => {
205
+ try {
206
+ const success = (window as any).ggbApplet.evalCommand(cmd);
207
+ return {
208
+ success: success,
209
+ error: success ? undefined : 'Command execution failed'
210
+ };
211
+ } catch (error) {
212
+ return {
213
+ success: false,
214
+ error: error.message || 'Unknown error'
215
+ };
216
+ }
217
+ }, command);
218
+
219
+ if (!result.success) {
220
+ throw new GeoGebraCommandError(result.error || 'Command failed', command);
221
+ }
222
+
223
+ logger.debug(`Command executed successfully on instance ${this.id}: ${command}`);
224
+ return result;
225
+ } catch (error) {
226
+ logger.error(`Command execution failed on instance ${this.id}`, { command, error });
227
+ if (error instanceof GeoGebraCommandError) {
228
+ throw error;
229
+ }
230
+ throw new GeoGebraCommandError(
231
+ `Failed to execute command: ${error instanceof Error ? error.message : String(error)}`,
232
+ command
233
+ );
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Execute command and get labels of created objects
239
+ */
240
+ async evalCommandGetLabels(command: string): Promise<string[]> {
241
+ this.ensureInitialized();
242
+ this.updateActivity();
243
+
244
+ try {
245
+ const labels = await this.page!.evaluate((cmd) => {
246
+ const result = (window as any).ggbApplet.evalCommandGetLabels(cmd);
247
+ return result ? result.split(',').filter((label: string) => label.trim()) : [];
248
+ }, command) as string[];
249
+
250
+ logger.debug(`Command executed on instance ${this.id}, labels: ${labels.join(', ')}`);
251
+ return labels;
252
+ } catch (error) {
253
+ logger.error(`Failed to execute command with labels on instance ${this.id}`, { command, error });
254
+ throw new GeoGebraCommandError(
255
+ `Failed to execute command: ${error instanceof Error ? error.message : String(error)}`,
256
+ command
257
+ );
258
+ }
259
+ }
260
+
261
+ /** Invoke a documented GeoGebra Apps API method against the live applet. */
262
+ async callAppsApi(method: string, args: unknown[] = [], options: {
263
+ waitForCallbackIndex?: number;
264
+ callbackTimeoutMs?: number;
265
+ callbacks?: Array<{ argumentIndex: number; id: string }>;
266
+ } = {}): Promise<unknown> {
267
+ this.ensureInitialized();
268
+ this.updateActivity();
269
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(method) || ['constructor','__proto__','prototype','toString','valueOf'].includes(method)) {
270
+ throw new Error('Invalid GeoGebra Apps API method name');
271
+ }
272
+ return this.page!.evaluate(async ({ method, args, waitForCallbackIndex, callbackTimeoutMs, callbacks }) => {
273
+ const api = (window as any).ggbApplet;
274
+ const target = api?.[method];
275
+ if (typeof target !== 'function') throw new Error(`GeoGebra Apps API method is unavailable: ${method}`);
276
+ const invocationArgs = [...args] as any[];
277
+ const callbackStore = ((window as any).__geogebraMcpCallbacks ??= { functions: {}, events: {} });
278
+ for (const descriptor of callbacks ?? []) {
279
+ if (!Number.isInteger(descriptor.argumentIndex) || descriptor.argumentIndex < 0 || descriptor.argumentIndex > 20 || !/^[A-Za-z0-9_-]{1,80}$/.test(descriptor.id)) {
280
+ throw new Error('Invalid persistent callback descriptor');
281
+ }
282
+ callbackStore.events[descriptor.id] ??= [];
283
+ callbackStore.functions[descriptor.id] ??= (...values: unknown[]) => {
284
+ const queue = callbackStore.events[descriptor.id];
285
+ queue.push(values);
286
+ if (queue.length > 100) queue.splice(0, queue.length - 100);
287
+ };
288
+ invocationArgs[descriptor.argumentIndex] = callbackStore.functions[descriptor.id];
289
+ }
290
+ if (waitForCallbackIndex !== undefined) {
291
+ const index = waitForCallbackIndex;
292
+ if (!Number.isInteger(index) || index < 0 || index > 20) throw new Error('Invalid callback argument index');
293
+ return await new Promise((resolve, reject) => {
294
+ const timer = setTimeout(() => reject(new Error(`GeoGebra API callback timed out: ${method}`)), callbackTimeoutMs);
295
+ invocationArgs[index] = (value: unknown) => { clearTimeout(timer); resolve(value); };
296
+ try {
297
+ const immediate = target.apply(api, invocationArgs);
298
+ if (immediate && typeof immediate.then === 'function') immediate.then(resolve, reject);
299
+ } catch (error) { clearTimeout(timer); reject(error); }
300
+ });
301
+ }
302
+ const result = target.apply(api, invocationArgs);
303
+ return result && typeof result.then === 'function' ? await result : result;
304
+ }, { method, args, ...options, callbackTimeoutMs: options.callbackTimeoutMs ?? 15000 });
305
+ }
306
+
307
+ async readAppsApiCallbackEvents(id: string, clear = true): Promise<unknown[][]> {
308
+ this.ensureInitialized();
309
+ if (!/^[A-Za-z0-9_-]{1,80}$/.test(id)) throw new Error('Invalid callback id');
310
+ return this.page!.evaluate(({ id, clear }) => {
311
+ const store = (window as any).__geogebraMcpCallbacks;
312
+ const events = store?.events?.[id] ?? [];
313
+ const copy = events.slice();
314
+ if (clear && store?.events?.[id]) store.events[id] = [];
315
+ return copy;
316
+ }, { id, clear });
317
+ }
318
+
319
+ /** Enumerate callable methods exposed by the currently loaded official applet API. */
320
+ async listAppsApiMethods(): Promise<string[]> {
321
+ this.ensureInitialized();
322
+ return this.page!.evaluate(() => {
323
+ const api = (window as any).ggbApplet;
324
+ const names = new Set<string>();
325
+ for (let object = api; object && object !== Object.prototype; object = Object.getPrototypeOf(object)) {
326
+ for (const name of Object.getOwnPropertyNames(object)) {
327
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(name) && typeof api[name] === 'function' && !['constructor','toString','valueOf'].includes(name)) names.add(name);
328
+ }
329
+ }
330
+ return [...names].sort();
331
+ });
332
+ }
333
+
334
+ /**
335
+ * Delete an object
336
+ */
337
+ async deleteObject(objName: string): Promise<boolean> {
338
+ this.ensureInitialized();
339
+ this.updateActivity();
340
+
341
+ try {
342
+ await this.page!.evaluate(`window.ggbApplet.deleteObject('${escapeForJavaScript(objName)}');`);
343
+ return true;
344
+ } catch (error) {
345
+ logger.error(`Failed to delete object ${objName} on instance ${this.id}`, error);
346
+ return false;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Check if object exists
352
+ */
353
+ async exists(objName: string): Promise<boolean> {
354
+ this.ensureInitialized();
355
+ this.updateActivity();
356
+
357
+ try {
358
+ const exists = await this.page!.evaluate((name) => {
359
+ return (window as any).ggbApplet.exists(name);
360
+ }, objName);
361
+ return exists;
362
+ } catch (error) {
363
+ logger.error(`Failed to check existence of object ${objName} on instance ${this.id}`, error);
364
+ return false;
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Check if object is defined
370
+ */
371
+ async isDefined(objName: string): Promise<boolean> {
372
+ this.ensureInitialized();
373
+ this.updateActivity();
374
+
375
+ try {
376
+ const defined = await this.page!.evaluate((name) => {
377
+ return (window as any).ggbApplet.isDefined(name);
378
+ }, objName);
379
+ return defined;
380
+ } catch (error) {
381
+ logger.error(`Failed to check if object ${objName} is defined on instance ${this.id}`, error);
382
+ return false;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Get all object names
388
+ */
389
+ async getAllObjectNames(type?: string): Promise<string[]> {
390
+ this.ensureInitialized();
391
+ this.updateActivity();
392
+
393
+ try {
394
+ const names = await this.page!.evaluate((objType) => {
395
+ return (window as any).ggbApplet.getAllObjectNames(objType);
396
+ }, type);
397
+ return names || [];
398
+ } catch (error) {
399
+ logger.error(`Failed to get object names on instance ${this.id}`, error);
400
+ return [];
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Set each object's caption to LaTeX name form ($name$) and show caption label mode.
406
+ */
407
+ async applyLatexCaptionDefaults(objectNames: string[], showLabel: boolean = true): Promise<void> {
408
+ this.ensureInitialized();
409
+ this.updateActivity();
410
+
411
+ if (objectNames.length === 0) {
412
+ return;
413
+ }
414
+
415
+ const uniqueNames = Array.from(new Set(objectNames.filter(name => name && name.trim())));
416
+
417
+ if (uniqueNames.length === 0) {
418
+ return;
419
+ }
420
+
421
+ try {
422
+ const result = await this.page!.evaluate((names, shouldShowLabel) => {
423
+ const applet = (window as any).ggbApplet;
424
+ const failed: string[] = [];
425
+
426
+ for (const name of names) {
427
+ try {
428
+ if (!applet.exists(name)) {
429
+ failed.push(name);
430
+ continue;
431
+ }
432
+
433
+ applet.setCaption(name, `$${name}$`);
434
+ applet.setLabelStyle(name, 3);
435
+
436
+ if (shouldShowLabel) {
437
+ applet.setLabelVisible(name, true);
438
+ }
439
+ } catch (_error) {
440
+ failed.push(name);
441
+ }
442
+ }
443
+
444
+ return { failed };
445
+ }, uniqueNames, showLabel);
446
+
447
+ if (Array.isArray(result?.failed) && result.failed.length > 0) {
448
+ logger.warn(`Failed to apply LaTeX caption defaults for ${result.failed.length} object(s)`, {
449
+ instanceId: this.id,
450
+ failedObjects: result.failed
451
+ });
452
+ }
453
+ } catch (error) {
454
+ logger.error(`Failed to apply LaTeX caption defaults on instance ${this.id}`, {
455
+ objectNames: uniqueNames,
456
+ error
457
+ });
458
+ throw error;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Apply default visual style settings to objects.
464
+ * - Point-like objects: point size
465
+ * - Line-like objects: line thickness
466
+ */
467
+ async applyDefaultStyleSettings(
468
+ objectNames: string[],
469
+ pointSize: number,
470
+ lineThickness: number
471
+ ): Promise<void> {
472
+ this.ensureInitialized();
473
+ this.updateActivity();
474
+
475
+ if (objectNames.length === 0) {
476
+ return;
477
+ }
478
+
479
+ const uniqueNames = Array.from(new Set(objectNames.filter(name => name && name.trim())));
480
+ if (uniqueNames.length === 0) {
481
+ return;
482
+ }
483
+
484
+ try {
485
+ const result = await this.page!.evaluate((names, pointSizeValue, lineThicknessValue) => {
486
+ const applet = (window as any).ggbApplet;
487
+ const failed: string[] = [];
488
+
489
+ const pointTypes = new Set(['point']);
490
+ const lineTypes = new Set([
491
+ 'line',
492
+ 'segment',
493
+ 'ray',
494
+ 'vector',
495
+ 'circle',
496
+ 'conic',
497
+ 'polygon',
498
+ 'function',
499
+ 'curve'
500
+ ]);
501
+
502
+ for (const name of names) {
503
+ try {
504
+ if (!applet.exists(name)) {
505
+ failed.push(name);
506
+ continue;
507
+ }
508
+
509
+ const rawType = String(applet.getObjectType(name) || '').toLowerCase();
510
+ if (pointTypes.has(rawType)) {
511
+ applet.setPointSize(name, pointSizeValue);
512
+ } else if (lineTypes.has(rawType)) {
513
+ applet.setLineThickness(name, lineThicknessValue);
514
+ }
515
+ } catch (_error) {
516
+ failed.push(name);
517
+ }
518
+ }
519
+
520
+ return { failed };
521
+ }, uniqueNames, pointSize, lineThickness);
522
+
523
+ if (Array.isArray(result?.failed) && result.failed.length > 0) {
524
+ logger.warn(`Failed to apply default style settings for ${result.failed.length} object(s)`, {
525
+ instanceId: this.id,
526
+ failedObjects: result.failed
527
+ });
528
+ }
529
+ } catch (error) {
530
+ logger.error(`Failed to apply default style settings on instance ${this.id}`, {
531
+ objectNames: uniqueNames,
532
+ pointSize,
533
+ lineThickness,
534
+ error
535
+ });
536
+ throw error;
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Get object information
542
+ */
543
+ async getObjectInfo(objName: string): Promise<GeoGebraObject | null> {
544
+ this.ensureInitialized();
545
+ this.updateActivity();
546
+
547
+ try {
548
+ const info = await this.page!.evaluate((name) => {
549
+ if (!(window as any).ggbApplet.exists(name)) {
550
+ return null;
551
+ }
552
+
553
+ const type = (window as any).ggbApplet.getObjectType(name);
554
+
555
+ // Base information that all objects have
556
+ const baseInfo = {
557
+ name,
558
+ type,
559
+ visible: (window as any).ggbApplet.getVisible(name),
560
+ defined: (window as any).ggbApplet.isDefined(name),
561
+ color: (window as any).ggbApplet.getColor(name)
562
+ };
563
+
564
+ // Try to get value and valueString safely
565
+ let value = null;
566
+ let valueString = null;
567
+ try {
568
+ value = (window as any).ggbApplet.getValue(name);
569
+ } catch (e) {
570
+ // Some objects don't have a numeric value
571
+ }
572
+
573
+ try {
574
+ valueString = (window as any).ggbApplet.getValueString(name);
575
+ } catch (e) {
576
+ // Some objects don't have a value string
577
+ }
578
+
579
+ // Only try to get coordinates for objects that support them
580
+ // Points, segments, lines, circles typically have coordinates
581
+ // Functions, curves, implicit curves typically don't
582
+ const coordinateTypes = ['point', 'segment', 'line', 'circle', 'polygon', 'vector'];
583
+ let coordinates = {};
584
+
585
+ if (coordinateTypes.includes(type.toLowerCase())) {
586
+ try {
587
+ coordinates = {
588
+ x: (window as any).ggbApplet.getXcoord(name),
589
+ y: (window as any).ggbApplet.getYcoord(name),
590
+ z: (window as any).ggbApplet.getZcoord(name)
591
+ };
592
+ } catch (e) {
593
+ // If coordinate access fails, just omit coordinates
594
+ }
595
+ }
596
+
597
+ return {
598
+ ...baseInfo,
599
+ value,
600
+ valueString,
601
+ ...coordinates
602
+ };
603
+ }, objName);
604
+
605
+ return info;
606
+ } catch (error) {
607
+ logger.error(`Failed to get object info for ${objName} on instance ${this.id}`, error);
608
+ return null;
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Get X coordinate of object
614
+ */
615
+ async getXcoord(objName: string): Promise<number> {
616
+ this.ensureInitialized();
617
+ this.updateActivity();
618
+
619
+ try {
620
+ const x = await this.page!.evaluate((name) => {
621
+ return (window as any).ggbApplet.getXcoord(name);
622
+ }, objName);
623
+ return x || 0;
624
+ } catch (error) {
625
+ logger.error(`Failed to get X coordinate of ${objName} on instance ${this.id}`, error);
626
+ return 0;
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Get Y coordinate of object
632
+ */
633
+ async getYcoord(objName: string): Promise<number> {
634
+ this.ensureInitialized();
635
+ this.updateActivity();
636
+
637
+ try {
638
+ const y = await this.page!.evaluate((name) => {
639
+ return (window as any).ggbApplet.getYcoord(name);
640
+ }, objName);
641
+ return y || 0;
642
+ } catch (error) {
643
+ logger.error(`Failed to get Y coordinate of ${objName} on instance ${this.id}`, error);
644
+ return 0;
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Get Z coordinate of object
650
+ */
651
+ async getZcoord(objName: string): Promise<number> {
652
+ this.ensureInitialized();
653
+ this.updateActivity();
654
+
655
+ try {
656
+ const z = await this.page!.evaluate((name) => {
657
+ return (window as any).ggbApplet.getZcoord(name);
658
+ }, objName);
659
+ return z || 0;
660
+ } catch (error) {
661
+ logger.error(`Failed to get Z coordinate of ${objName} on instance ${this.id}`, error);
662
+ return 0;
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Get value of object
668
+ */
669
+ async getValue(objName: string): Promise<number> {
670
+ this.ensureInitialized();
671
+ this.updateActivity();
672
+
673
+ try {
674
+ const value = await this.page!.evaluate((name) => {
675
+ return (window as any).ggbApplet.getValue(name);
676
+ }, objName);
677
+ return value || 0;
678
+ } catch (error) {
679
+ logger.error(`Failed to get value of ${objName} on instance ${this.id}`, error);
680
+ return 0;
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Get value string of object
686
+ */
687
+ async getValueString(objName: string): Promise<string> {
688
+ this.ensureInitialized();
689
+ this.updateActivity();
690
+
691
+ try {
692
+ const valueStr = await this.page!.evaluate((name) => {
693
+ return (window as any).ggbApplet.getValueString(name);
694
+ }, objName);
695
+ return valueStr || '';
696
+ } catch (error) {
697
+ logger.error(`Failed to get value string of ${objName} on instance ${this.id}`, error);
698
+ return '';
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Clear construction
704
+ */
705
+ async newConstruction(): Promise<void> {
706
+ this.ensureInitialized();
707
+ this.updateActivity();
708
+
709
+ try {
710
+ await this.page!.evaluate(() => {
711
+ (window as any).ggbApplet.newConstruction();
712
+ });
713
+ logger.debug(`Construction cleared on instance ${this.id}`);
714
+ } catch (error) {
715
+ logger.error(`Failed to clear construction on instance ${this.id}`, error);
716
+ throw new GeoGebraError(`Failed to clear construction: ${error instanceof Error ? error.message : String(error)}`);
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Reset construction
722
+ */
723
+ async reset(): Promise<void> {
724
+ this.ensureInitialized();
725
+ this.updateActivity();
726
+
727
+ try {
728
+ await this.page!.evaluate(() => {
729
+ (window as any).ggbApplet.reset();
730
+ });
731
+ logger.debug(`Construction reset on instance ${this.id}`);
732
+ } catch (error) {
733
+ logger.error(`Failed to reset construction on instance ${this.id}`, error);
734
+ throw new GeoGebraError(`Failed to reset construction: ${error instanceof Error ? error.message : String(error)}`);
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Refresh views
740
+ */
741
+ async refreshViews(): Promise<void> {
742
+ this.ensureInitialized();
743
+ this.updateActivity();
744
+
745
+ try {
746
+ await this.page!.evaluate(() => {
747
+ (window as any).ggbApplet.refreshViews();
748
+ });
749
+ logger.debug(`Views refreshed on instance ${this.id}`);
750
+ } catch (error) {
751
+ logger.error(`Failed to refresh views on instance ${this.id}`, error);
752
+ throw new GeoGebraError(`Failed to refresh views: ${error instanceof Error ? error.message : String(error)}`);
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Set coordinate system bounds
758
+ */
759
+ async setCoordSystem(xmin: number, xmax: number, ymin: number, ymax: number): Promise<void> {
760
+ this.ensureInitialized();
761
+ this.updateActivity();
762
+
763
+ try {
764
+ await this.page!.evaluate((xmin, xmax, ymin, ymax) => {
765
+ (window as any).ggbApplet.setCoordSystem(xmin, xmax, ymin, ymax);
766
+ }, xmin, xmax, ymin, ymax);
767
+ logger.debug(`Coordinate system set on instance ${this.id}: x[${xmin}, ${xmax}], y[${ymin}, ${ymax}]`);
768
+ } catch (error) {
769
+ logger.error(`Failed to set coordinate system on instance ${this.id}`, error);
770
+ throw new GeoGebraError(`Failed to set coordinate system: ${error instanceof Error ? error.message : String(error)}`);
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Set axes visibility
776
+ */
777
+ async setAxesVisible(xAxis: boolean, yAxis: boolean): Promise<void> {
778
+ this.ensureInitialized();
779
+ this.updateActivity();
780
+
781
+ try {
782
+ await this.page!.evaluate((xAxis, yAxis) => {
783
+ (window as any).ggbApplet.setAxesVisible(xAxis, yAxis);
784
+ }, xAxis, yAxis);
785
+ logger.debug(`Axes visibility set on instance ${this.id}: x=${xAxis}, y=${yAxis}`);
786
+ } catch (error) {
787
+ logger.error(`Failed to set axes visibility on instance ${this.id}`, error);
788
+ throw new GeoGebraError(`Failed to set axes visibility: ${error instanceof Error ? error.message : String(error)}`);
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Set grid visibility
794
+ */
795
+ async setGridVisible(visible: boolean): Promise<void> {
796
+ this.ensureInitialized();
797
+ this.updateActivity();
798
+
799
+ try {
800
+ await this.page!.evaluate((visible) => {
801
+ (window as any).ggbApplet.setGridVisible(visible);
802
+ }, visible);
803
+
804
+ logger.debug(`Grid visibility set on instance ${this.id}: ${visible}`);
805
+ } catch (error) {
806
+ logger.error(`Failed to set grid visibility on instance ${this.id}`, error);
807
+ throw new GeoGebraError(`Failed to set grid visibility: ${error instanceof Error ? error.message : String(error)}`);
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Check if GeoGebra is ready
813
+ */
814
+ async isReady(): Promise<boolean> {
815
+ if (!this.page) return false;
816
+
817
+ try {
818
+ const ready = await this.page.evaluate(() => {
819
+ return (window as any).ggbReady === true && (window as any).ggbApplet;
820
+ });
821
+ return ready;
822
+ } catch (error) {
823
+ logger.error(`Failed to check ready state on instance ${this.id}`, error);
824
+ return false;
825
+ }
826
+ }
827
+
828
+ /**
829
+ * Export construction as PNG (base64) with enhanced parameters
830
+ * GEB-17: Enhanced with validation, retry logic, and better error handling
831
+ */
832
+ async exportPNG(scale: number = 1, transparent: boolean = false, dpi: number = 72, width?: number, height?: number): Promise<string> {
833
+ this.ensureInitialized();
834
+ this.updateActivity();
835
+
836
+ // GEB-17: Validate export readiness before attempting
837
+ const readiness = await this.validateExportReadiness('png');
838
+ if (!readiness.ready) {
839
+ throw new GeoGebraError(`PNG export not ready: ${readiness.issues.join(', ')}. Recommendations: ${readiness.recommendations.join(', ')}`);
840
+ }
841
+
842
+ return this.retryOperation(async () => {
843
+ const pngBase64 = await this.page!.evaluate((scale, transparent, dpi, width, height) => {
844
+ try {
845
+ // GEB-17: Enhanced applet availability check
846
+ const applet = (window as any).ggbApplet;
847
+ if (!applet) {
848
+ throw new Error('GeoGebra applet not available');
849
+ }
850
+
851
+ if (typeof applet.getPNGBase64 !== 'function') {
852
+ throw new Error('getPNGBase64 method not available on applet');
853
+ }
854
+
855
+ // Calculate effective scale if width/height provided
856
+ let effectiveScale = scale;
857
+ if (width !== undefined && height !== undefined) {
858
+ // Get current dimensions with better error handling
859
+ let graphics;
860
+ try {
861
+ graphics = applet.getGraphicsOptions ? applet.getGraphicsOptions() : { width: 800, height: 600 };
862
+ } catch (e) {
863
+ // Fallback to default dimensions if getGraphicsOptions fails
864
+ graphics = { width: 800, height: 600 };
865
+ }
866
+
867
+ const currentWidth = graphics.width || 800;
868
+ const currentHeight = graphics.height || 600;
869
+ const scaleX = width / currentWidth;
870
+ const scaleY = height / currentHeight;
871
+ effectiveScale = Math.min(scaleX, scaleY);
872
+ }
873
+
874
+ // GEB-17: Enhanced PNG export with better error handling and validation
875
+ let result = null;
876
+ let lastError = null;
877
+
878
+ // Try primary method with full parameters
879
+ try {
880
+ result = applet.getPNGBase64(effectiveScale, transparent, dpi);
881
+ if (result && typeof result === 'string' && result.length > 0) {
882
+ return result; // Success with primary method
883
+ }
884
+ } catch (e1) {
885
+ lastError = e1;
886
+ // Continue to fallback methods
887
+ }
888
+
889
+ // Try without dpi parameter
890
+ try {
891
+ result = applet.getPNGBase64(effectiveScale, transparent);
892
+ if (result && typeof result === 'string' && result.length > 0) {
893
+ return result; // Success with fallback method
894
+ }
895
+ } catch (e2) {
896
+ lastError = e2;
897
+ // Continue to next fallback
898
+ }
899
+
900
+ // Try simplest approach with just scale
901
+ try {
902
+ result = applet.getPNGBase64(effectiveScale);
903
+ if (result && typeof result === 'string' && result.length > 0) {
904
+ return result; // Success with simple method
905
+ }
906
+ } catch (e3) {
907
+ lastError = e3;
908
+ // Continue to final fallback
909
+ }
910
+
911
+ // Try with default scale as last resort
912
+ try {
913
+ result = applet.getPNGBase64(1);
914
+ if (result && typeof result === 'string' && result.length > 0) {
915
+ return result; // Success with default scale
916
+ }
917
+ } catch (e4) {
918
+ lastError = e4;
919
+ }
920
+
921
+ // If we get here, all methods failed
922
+ throw new Error(`All PNG export methods failed. Last error: ${lastError ? lastError.message : 'Unknown error'}. Result was: ${typeof result} with length ${result ? result.length : 'undefined'}`);
923
+
924
+ } catch (error) {
925
+ throw new Error(`PNG export failed: ${error.message || error}`);
926
+ }
927
+ }, scale, transparent, dpi, width, height);
928
+
929
+ // GEB-17: Enhanced validation of the result
930
+ if (!pngBase64) {
931
+ throw new Error('PNG export returned null or undefined');
932
+ }
933
+
934
+ if (typeof pngBase64 !== 'string') {
935
+ throw new Error(`PNG export returned invalid type: ${typeof pngBase64}`);
936
+ }
937
+
938
+ if (pngBase64.length === 0) {
939
+ throw new Error('PNG export returned empty string');
940
+ }
941
+
942
+ // Enhanced base64 validation
943
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(pngBase64)) {
944
+ throw new Error(`Result is not valid base64. Length: ${pngBase64.length}, starts with: ${pngBase64.substring(0, 50)}`);
945
+ }
946
+
947
+ // Additional check for minimum reasonable size (base64 should be substantial for a real image)
948
+ if (pngBase64.length < 100) {
949
+ throw new Error(`PNG result suspiciously small (${pngBase64.length} characters): ${pngBase64}`);
950
+ }
951
+
952
+ logger.debug(`PNG exported from instance ${this.id} with scale ${scale}, transparent ${transparent}, dpi ${dpi}, dimensions ${width}x${height}, result length: ${pngBase64.length}`);
953
+ return pngBase64;
954
+ }, 3, 1000, 'PNG export');
955
+ }
956
+
957
+ /**
958
+ * Export construction as SVG
959
+ * GEB-17: Enhanced with better validation and error handling, no more placeholder responses
960
+ */
961
+ /** Raw PGF is internal input to the mandatory Gikz pipeline. */
962
+ async exportNativeTikZ(): Promise<string> { return this.nativeExport('tikz'); }
963
+ async exportSVG(): Promise<string> { return this.nativeExport('svg'); }
964
+ async exportPDF(): Promise<string> { return this.nativeExport('pdf'); }
965
+ async exportGGB(): Promise<string> { return this.nativeExport('ggb'); }
966
+
967
+ private async nativeExport(format: 'svg' | 'pdf' | 'ggb' | 'tikz'): Promise<string> {
968
+ this.ensureInitialized();
969
+ this.updateActivity();
970
+ const result = await this.page!.evaluate((kind) => new Promise<string>((resolve, reject) => {
971
+ const api = (window as any).ggbApplet;
972
+ const timer = setTimeout(() => reject(new Error(kind + ' export timed out')), 20000);
973
+ const done = (value: string) => {
974
+ clearTimeout(timer);
975
+ if (!value || typeof value !== 'string') reject(new Error(kind + ' export returned no data'));
976
+ else resolve(value);
977
+ };
978
+ try {
979
+ if (kind === 'ggb') api.getBase64(done);
980
+ else if (kind === 'svg') api.exportSVG(done);
981
+ else if (kind === 'tikz') api.exportPGF(done);
982
+ else api.exportPDF(1, done);
983
+ } catch (e) { clearTimeout(timer); reject(e); }
984
+ }), format);
985
+ if (format === 'tikz') {
986
+ if (!result.includes('\\begin{tikzpicture}')) throw new GeoGebraError('Invalid native TikZ export');
987
+ return result;
988
+ }
989
+ if (format === 'svg') {
990
+ if (!result.includes('<svg')) throw new GeoGebraError('Invalid SVG export');
991
+ return result;
992
+ }
993
+ const base64 = result.replace(/^data:[^,]+,/, '');
994
+ const bytes = Buffer.from(base64, 'base64');
995
+ if (format === 'ggb' && bytes.subarray(0, 2).toString() !== 'PK') throw new GeoGebraError('GGB export is not a ZIP archive');
996
+ if (format === 'pdf' && bytes.subarray(0, 5).toString() !== '%PDF-') throw new GeoGebraError('Invalid native PDF export');
997
+ return base64;
998
+ }
999
+
1000
+ /** Load a generated viewer and wait for its native applet and responsive layout. */
1001
+ async renderHtml(html: string): Promise<void> {
1002
+ this.ensureInitialized();
1003
+ // A fresh document prevents lexical globals and old applet callbacks leaking between renders.
1004
+ await this.page!.goto("about:blank");
1005
+ await this.page!.setContent(html);
1006
+ await this.page!.waitForFunction('window.ggbReady || window.ggbError', {timeout:60000});
1007
+ const error=await this.page!.evaluate(()=>(window as any).ggbError);
1008
+ if(error)throw new GeoGebraConnectionError(error);
1009
+ await this.page!.waitForFunction(()=>{
1010
+ const view=JSON.parse((window as any).ggbApplet.getViewProperties(1));
1011
+ return view.width>0&&view.height>0&&Math.abs(view.invXscale/view.invYscale-1)<1e-6;
1012
+ },{timeout:10000});
1013
+ }
1014
+ async capturePreview(): Promise<Uint8Array> {
1015
+ this.ensureInitialized();
1016
+ // Native LaTeX loads fonts after appletOnLoad; capture the completed paint.
1017
+ // GeoGebra maintains background requests, so network idle is not a readiness condition.
1018
+ await this.page!.evaluate(()=>new Promise(resolve=>setTimeout(resolve,2000)));
1019
+ await this.page!.evaluate(async()=>{await document.fonts.ready;await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));});
1020
+ return this.page!.screenshot({type:'png'});
1021
+ }
1022
+
1023
+ async getConstructionXML(): Promise<string> {
1024
+ this.ensureInitialized();
1025
+ return this.page!.evaluate(() => String((window as any).ggbApplet.getXML()));
1026
+ }
1027
+
1028
+ /**
1029
+ * Validate that a file path is within the workspace directory to prevent path traversal attacks
1030
+ * @param filePath - The file path to validate
1031
+ * @param workspaceDir - The workspace directory
1032
+ * @returns The validated absolute path
1033
+ * @throws Error if path is outside workspace
1034
+ */
1035
+ public validateFilePath(filePath: string, workspaceDir: string): string {
1036
+ const path = require('path');
1037
+ const fullPath = path.resolve(workspaceDir, filePath);
1038
+ const normalizedWorkspace = path.resolve(workspaceDir);
1039
+
1040
+ // Check if the resolved path is within the workspace
1041
+ if (!fullPath.startsWith(normalizedWorkspace + path.sep) && fullPath !== normalizedWorkspace) {
1042
+ throw new Error(`Path traversal detected: ${filePath} resolves outside workspace`);
1043
+ }
1044
+
1045
+ return fullPath;
1046
+ }
1047
+
1048
+ /**
1049
+ * Calculate optimal view coordinates to fit all visible objects
1050
+ * @param padding - Padding percentage around objects (default: 0.2 for 20% margin)
1051
+ * @param minRange - Minimum range for each axis (default: 2)
1052
+ * @returns View coordinates {xmin, xmax, ymin, ymax} or null if no visible points
1053
+ */
1054
+ async calculateAutoZoomCoordinates(padding: number = 0.2, minRange: number = 2): Promise<{xmin: number, xmax: number, ymin: number, ymax: number} | null> {
1055
+ this.ensureInitialized();
1056
+ this.updateActivity();
1057
+
1058
+ const objectNames = await this.getAllObjectNames();
1059
+ const allObjects = [];
1060
+ for (const name of objectNames) {
1061
+ const info = await this.getObjectInfo(name);
1062
+ if (info) {
1063
+ allObjects.push(info);
1064
+ }
1065
+ }
1066
+
1067
+ const visiblePoints = allObjects.filter((obj: any) =>
1068
+ obj.type === 'point' && obj.visible && obj.defined &&
1069
+ typeof obj.x === 'number' && typeof obj.y === 'number'
1070
+ );
1071
+
1072
+ if (visiblePoints.length === 0) {
1073
+ return null;
1074
+ }
1075
+
1076
+ let minX = Infinity;
1077
+ let maxX = -Infinity;
1078
+ let minY = Infinity;
1079
+ let maxY = -Infinity;
1080
+
1081
+ for (const point of visiblePoints) {
1082
+ minX = Math.min(minX, point.x!);
1083
+ maxX = Math.max(maxX, point.x!);
1084
+ minY = Math.min(minY, point.y!);
1085
+ maxY = Math.max(maxY, point.y!);
1086
+ }
1087
+
1088
+ let rangeX = maxX - minX;
1089
+ let rangeY = maxY - minY;
1090
+
1091
+ // Ensure minimum range
1092
+ if (rangeX < minRange) {
1093
+ const center = (minX + maxX) / 2;
1094
+ minX = center - minRange / 2;
1095
+ maxX = center + minRange / 2;
1096
+ rangeX = minRange;
1097
+ }
1098
+
1099
+ if (rangeY < minRange) {
1100
+ const center = (minY + maxY) / 2;
1101
+ minY = center - minRange / 2;
1102
+ maxY = center + minRange / 2;
1103
+ rangeY = minRange;
1104
+ }
1105
+
1106
+ // Apply padding
1107
+ const padX = rangeX * padding;
1108
+ const padY = rangeY * padding;
1109
+
1110
+ return {
1111
+ xmin: minX - padX,
1112
+ xmax: maxX + padX,
1113
+ ymin: minY - padY,
1114
+ ymax: maxY + padY
1115
+ };
1116
+ }
1117
+
1118
+ /**
1119
+ * Apply auto-zoom to fit all visible objects
1120
+ * @param padding - Padding percentage around objects (default: 0.2 for 20% margin)
1121
+ * @param minRange - Minimum range for each axis (default: 2)
1122
+ * @returns true if auto-zoom was applied, false if no visible points
1123
+ */
1124
+ async applyAutoZoom(padding: number = 0.2, minRange: number = 2): Promise<boolean> {
1125
+ this.ensureInitialized();
1126
+ if (!Number.isFinite(padding) || padding < 0 || padding > 2 || !Number.isFinite(minRange) || minRange <= 0) throw new GeoGebraError('Invalid auto zoom padding or minimum range');
1127
+ return this.page!.evaluate(async (pad, minimum) => {
1128
+ const api = (window as any).ggbApplet;
1129
+ if (!api.getAllObjectNames().length) return false;
1130
+ // Native fitting includes conic extents. Preserve equal coordinate scales.
1131
+ api.showAllObjects();
1132
+ // GeoGebra animates fitting; read the bounds after that animation settles.
1133
+ await new Promise(resolve => setTimeout(resolve, 600));
1134
+ const view = JSON.parse(api.getViewProperties(1));
1135
+ const spanX = Math.max(view.width * view.invXscale * (1+2*pad), minimum);
1136
+ const spanY = Math.max(view.height * view.invYscale * (1+2*pad), minimum);
1137
+ const scale = Math.max(spanX/view.width, spanY/view.height);
1138
+ const cx = view.xMin + view.width*view.invXscale/2;
1139
+ const cy = view.yMin + view.height*view.invYscale/2;
1140
+ api.setCoordSystem(cx-view.width*scale/2,cx+view.width*scale/2,cy-view.height*scale/2,cy+view.height*scale/2);
1141
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
1142
+ return true;
1143
+ }, padding, minRange);
1144
+ }
1145
+
1146
+ /**
1147
+ * Load a GeoGebra construction from a .ggb file
1148
+ * @param ggbBase64OrPath - Either base64-encoded .ggb content or file path
1149
+ */
1150
+ async loadGGB(ggbBase64OrPath: string): Promise<void> {
1151
+ this.ensureInitialized();
1152
+ this.updateActivity();
1153
+
1154
+ return this.retryOperation(async () => {
1155
+ let ggbBase64 = ggbBase64OrPath;
1156
+
1157
+ // Check if it's a file path (contains path separators or ends with .ggb)
1158
+ if (!(/^[A-Za-z0-9+/]+={0,2}$/.test(ggbBase64OrPath) && Buffer.from(ggbBase64OrPath, 'base64').subarray(0, 2).toString() === 'PK')) {
1159
+ // It's a file path, read the file
1160
+ const fs = await import('fs/promises');
1161
+ const path = await import('path');
1162
+
1163
+ // Resolve path relative to workspace or current directory
1164
+ const workspaceDir = currentWorkspace();
1165
+ const fullPath = this.validateFilePath(ggbBase64OrPath, workspaceDir);
1166
+
1167
+ try {
1168
+ await fs.access(fullPath);
1169
+ } catch (error) {
1170
+ throw new Error(`GGB file not found: ${fullPath}`);
1171
+ }
1172
+
1173
+ // Read file and convert to base64 (async)
1174
+ const buffer = await fs.readFile(fullPath);
1175
+ ggbBase64 = buffer.toString('base64');
1176
+ }
1177
+
1178
+ // Validate base64 format
1179
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(ggbBase64)) {
1180
+ throw new Error('Invalid base64 format for GGB data');
1181
+ }
1182
+
1183
+ await this.page!.evaluate((base64Data: string) => new Promise<void>((resolve, reject) => {
1184
+ const timer = setTimeout(() => reject(new Error('GGB load timed out')), 20000);
1185
+ try {
1186
+ (window as any).ggbApplet.setBase64(base64Data, () => { clearTimeout(timer); resolve(); });
1187
+ } catch (error) { clearTimeout(timer); reject(error); }
1188
+ }), ggbBase64);
1189
+
1190
+ logger.debug(`GGB file loaded into instance ${this.id}`);
1191
+ }, 3, 1000, 'GGB load');
1192
+ }
1193
+
1194
+ /**
1195
+ * Export animation as GIF or video frames
1196
+ * GEB-17: Enhanced with comprehensive applet availability checks
1197
+ */
1198
+ async exportAnimation(options: {
1199
+ duration?: number;
1200
+ frameRate?: number;
1201
+ format?: 'gif' | 'frames';
1202
+ width?: number;
1203
+ height?: number;
1204
+ } = {}): Promise<string | string[]> {
1205
+ this.ensureInitialized();
1206
+ this.updateActivity();
1207
+
1208
+ // GEB-17: Validate export readiness before attempting
1209
+ const readiness = await this.validateExportReadiness('animation');
1210
+ if (!readiness.ready) {
1211
+ throw new GeoGebraError(`Animation export not ready: ${readiness.issues.join(', ')}. Recommendations: ${readiness.recommendations.join(', ')}`);
1212
+ }
1213
+
1214
+ const {
1215
+ duration = 5000, // 5 seconds
1216
+ frameRate = 10, // 10 fps
1217
+ format = 'frames',
1218
+ width,
1219
+ height
1220
+ } = options;
1221
+
1222
+ return this.retryOperation(async () => {
1223
+ // GEB-17: Enhanced applet availability and animation readiness check
1224
+ const animationCheck = await this.page!.evaluate(() => {
1225
+ const applet = (window as any).ggbApplet;
1226
+
1227
+ if (!applet) {
1228
+ return { ready: false, error: 'GeoGebra applet not available' };
1229
+ }
1230
+
1231
+ if (typeof applet.startAnimation !== 'function' || typeof applet.stopAnimation !== 'function') {
1232
+ return { ready: false, error: 'Animation methods not available on applet' };
1233
+ }
1234
+
1235
+ if (typeof applet.isAnimationRunning !== 'function') {
1236
+ return { ready: false, error: 'Animation status method not available' };
1237
+ }
1238
+
1239
+ // Check if there are any objects that can be animated
1240
+ try {
1241
+ const allObjects = applet.getAllObjectNames();
1242
+ if (!allObjects || allObjects.length === 0) {
1243
+ return { ready: false, error: 'No objects available for animation' };
1244
+ }
1245
+ } catch (e) {
1246
+ return { ready: false, error: 'Cannot access object list for animation' };
1247
+ }
1248
+
1249
+ return { ready: true, error: null };
1250
+ });
1251
+
1252
+ if (!animationCheck.ready) {
1253
+ throw new Error(`Animation export failed: ${animationCheck.error}`);
1254
+ }
1255
+
1256
+ const frameInterval = 1000 / frameRate;
1257
+ const totalFrames = Math.ceil(duration / frameInterval);
1258
+ const frames: string[] = [];
1259
+
1260
+ logger.debug(`Starting animation capture for instance ${this.id}: ${totalFrames} frames over ${duration}ms`);
1261
+
1262
+ try {
1263
+ // Start animation
1264
+ await this.page!.evaluate(() => {
1265
+ (window as any).ggbApplet.startAnimation();
1266
+ });
1267
+
1268
+ // Capture frames
1269
+ for (let i = 0; i < totalFrames; i++) {
1270
+ // Wait for frame interval
1271
+ await new Promise(resolve => setTimeout(resolve, frameInterval));
1272
+
1273
+ // Capture frame as PNG
1274
+ try {
1275
+ const frameData = await this.exportPNG(1, false, 72, width, height);
1276
+ frames.push(frameData);
1277
+ logger.debug(`Captured frame ${i + 1}/${totalFrames} for instance ${this.id}`);
1278
+ } catch (frameError) {
1279
+ logger.warn(`Failed to capture frame ${i + 1}/${totalFrames} for instance ${this.id}:`, frameError);
1280
+ // Continue with next frame rather than failing completely
1281
+ }
1282
+ }
1283
+
1284
+ // Stop animation
1285
+ await this.page!.evaluate(() => {
1286
+ (window as any).ggbApplet.stopAnimation();
1287
+ });
1288
+
1289
+ if (frames.length === 0) {
1290
+ throw new Error('No frames were successfully captured');
1291
+ }
1292
+
1293
+ logger.debug(`Animation capture completed for instance ${this.id}: ${frames.length} frames captured`);
1294
+
1295
+ if (format === 'frames') {
1296
+ return frames;
1297
+ } else {
1298
+ // For GIF format, we would need additional processing
1299
+ // For now, return the frames and let the caller handle GIF creation
1300
+ logger.warn('GIF format not yet implemented, returning frames');
1301
+ return frames;
1302
+ }
1303
+
1304
+ } catch (error) {
1305
+ // Ensure animation is stopped even if capture fails
1306
+ try {
1307
+ await this.page!.evaluate(() => {
1308
+ (window as any).ggbApplet.stopAnimation();
1309
+ });
1310
+ } catch (stopError) {
1311
+ logger.error(`Failed to stop animation after capture error:`, stopError);
1312
+ }
1313
+
1314
+ throw error;
1315
+ }
1316
+ }, 2, 3000, 'Animation export'); // Fewer retries, longer delay for animation
1317
+ }
1318
+
1319
+ /**
1320
+ * Cleanup resources
1321
+ */
1322
+ async cleanup(): Promise<void> {
1323
+ logger.debug(`Cleaning up GeoGebra instance ${this.id}`);
1324
+
1325
+ try {
1326
+ if (this.page && !this.page.isClosed()) {
1327
+ await this.page.close();
1328
+ }
1329
+ } catch (error) {
1330
+ logger.error(`Error closing page for instance ${this.id}`, error);
1331
+ }
1332
+
1333
+ try {
1334
+ if (this.browser) {
1335
+ await this.browser.close();
1336
+ }
1337
+ } catch (error) {
1338
+ logger.error(`Error closing browser for instance ${this.id}`, error);
1339
+ }
1340
+
1341
+ this.page = undefined;
1342
+ this.browser = undefined;
1343
+ this.isInitialized = false;
1344
+
1345
+ logger.debug(`GeoGebra instance ${this.id} cleaned up successfully`);
1346
+ }
1347
+
1348
+ /**
1349
+ * Get instance state
1350
+ */
1351
+ getState() {
1352
+ return {
1353
+ id: this.id,
1354
+ isReady: this.isInitialized,
1355
+ lastActivity: this.lastActivity,
1356
+ config: this.config
1357
+ };
1358
+ }
1359
+
1360
+ /**
1361
+ * GEB-17: Enhanced diagnostic method to check applet health
1362
+ */
1363
+ async checkAppletHealth(): Promise<{
1364
+ isHealthy: boolean;
1365
+ issues: string[];
1366
+ capabilities: {
1367
+ hasApplet: boolean;
1368
+ hasEvalCommand: boolean;
1369
+ hasExportMethods: boolean;
1370
+ hasAnimationMethods: boolean;
1371
+ };
1372
+ exportAvailability: {
1373
+ png: boolean;
1374
+ svg: boolean;
1375
+ pdf: boolean;
1376
+ };
1377
+ }> {
1378
+ this.ensureInitialized();
1379
+
1380
+ try {
1381
+ const healthCheck = await this.page!.evaluate(() => {
1382
+ const issues: string[] = [];
1383
+ const applet = (window as any).ggbApplet;
1384
+
1385
+ // Check basic applet availability
1386
+ const hasApplet = !!applet;
1387
+ if (!hasApplet) {
1388
+ issues.push('GeoGebra applet not found');
1389
+ }
1390
+
1391
+ // Check core methods
1392
+ const hasEvalCommand = hasApplet && typeof applet.evalCommand === 'function';
1393
+ if (!hasEvalCommand) {
1394
+ issues.push('evalCommand method not available');
1395
+ }
1396
+
1397
+ // Check export methods
1398
+ const hasPNGExport = hasApplet && typeof applet.getPNGBase64 === 'function';
1399
+ const hasSVGExport = hasApplet && (typeof applet.exportSVG === 'function' || typeof applet.getSVG === 'function');
1400
+
1401
+ if (!hasPNGExport) {
1402
+ issues.push('PNG export method (getPNGBase64) not available');
1403
+ }
1404
+ if (!hasSVGExport) {
1405
+ issues.push('SVG export methods not available');
1406
+ }
1407
+
1408
+ // Check animation methods
1409
+ const hasAnimationMethods = hasApplet &&
1410
+ typeof applet.startAnimation === 'function' &&
1411
+ typeof applet.stopAnimation === 'function';
1412
+
1413
+ if (!hasAnimationMethods) {
1414
+ issues.push('Animation methods not available');
1415
+ }
1416
+
1417
+ // Test a simple command to verify functionality
1418
+ let commandWorks = false;
1419
+ if (hasEvalCommand) {
1420
+ try {
1421
+ // Try a harmless command
1422
+ const result = typeof applet.evalCommand === 'function' && typeof applet.getAllObjectNames === 'function';
1423
+ commandWorks = Boolean(result);
1424
+ } catch (e) {
1425
+ issues.push('Command execution test failed: ' + e.message);
1426
+ }
1427
+ }
1428
+
1429
+ return {
1430
+ issues,
1431
+ capabilities: {
1432
+ hasApplet,
1433
+ hasEvalCommand: hasEvalCommand && commandWorks,
1434
+ hasExportMethods: hasPNGExport && hasSVGExport,
1435
+ hasAnimationMethods
1436
+ },
1437
+ exportAvailability: {
1438
+ png: hasPNGExport,
1439
+ svg: hasSVGExport,
1440
+ pdf: hasApplet && typeof applet.exportPDF === 'function'
1441
+ }
1442
+ };
1443
+ });
1444
+
1445
+ const isHealthy = healthCheck.issues.length === 0;
1446
+
1447
+ return {
1448
+ isHealthy,
1449
+ ...healthCheck
1450
+ };
1451
+ } catch (error) {
1452
+ return {
1453
+ isHealthy: false,
1454
+ issues: [`Health check failed: ${error instanceof Error ? error.message : String(error)}`],
1455
+ capabilities: {
1456
+ hasApplet: false,
1457
+ hasEvalCommand: false,
1458
+ hasExportMethods: false,
1459
+ hasAnimationMethods: false
1460
+ },
1461
+ exportAvailability: {
1462
+ png: false,
1463
+ svg: false,
1464
+ pdf: false
1465
+ }
1466
+ };
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * GEB-17: Enhanced method to validate export readiness before attempting export
1472
+ */
1473
+ async validateExportReadiness(exportType: 'png' | 'svg' | 'pdf' | 'animation'): Promise<{
1474
+ ready: boolean;
1475
+ issues: string[];
1476
+ recommendations: string[];
1477
+ }> {
1478
+ this.ensureInitialized();
1479
+
1480
+ const health = await this.checkAppletHealth();
1481
+ const issues: string[] = [];
1482
+ const recommendations: string[] = [];
1483
+
1484
+ if (!health.isHealthy) {
1485
+ issues.push(...health.issues);
1486
+ recommendations.push('Reinitialize the GeoGebra instance');
1487
+ }
1488
+
1489
+ // Type-specific checks
1490
+ switch (exportType) {
1491
+ case 'png':
1492
+ if (!health.exportAvailability.png) {
1493
+ issues.push('PNG export method not available');
1494
+ recommendations.push('Check GeoGebra applet initialization');
1495
+ }
1496
+ break;
1497
+
1498
+ case 'svg':
1499
+ if (!health.exportAvailability.svg) {
1500
+ issues.push('SVG export method not available');
1501
+ recommendations.push('Check GeoGebra applet initialization');
1502
+ }
1503
+ break;
1504
+
1505
+ case 'pdf':
1506
+ // PDF uses browser PDF generation, check page availability
1507
+ if (!this.page || this.page.isClosed()) {
1508
+ issues.push('Browser page not available for PDF generation');
1509
+ recommendations.push('Reinitialize the browser instance');
1510
+ }
1511
+ break;
1512
+
1513
+ case 'animation':
1514
+ if (!health.capabilities.hasAnimationMethods) {
1515
+ issues.push('Animation methods not available');
1516
+ recommendations.push('Check GeoGebra applet initialization');
1517
+ }
1518
+
1519
+ // Check if there are objects to animate
1520
+ try {
1521
+ const objects = await this.getAllObjectNames();
1522
+ if (objects.length === 0) {
1523
+ issues.push('No objects available for animation');
1524
+ recommendations.push('Create geometric objects before exporting animation');
1525
+ }
1526
+ } catch (e) {
1527
+ issues.push('Cannot check for objects to animate');
1528
+ }
1529
+ break;
1530
+ }
1531
+
1532
+ return {
1533
+ ready: issues.length === 0,
1534
+ issues,
1535
+ recommendations
1536
+ };
1537
+ }
1538
+
1539
+ /**
1540
+ * GEB-17: Retry mechanism for failed operations
1541
+ */
1542
+ private async retryOperation<T>(
1543
+ operation: () => Promise<T>,
1544
+ maxRetries: number = 3,
1545
+ delay: number = 1000,
1546
+ operationName: string = 'operation'
1547
+ ): Promise<T> {
1548
+ let lastError: Error | unknown;
1549
+
1550
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1551
+ try {
1552
+ logger.debug(`Attempting ${operationName} (attempt ${attempt}/${maxRetries})`);
1553
+ return await operation();
1554
+ } catch (error) {
1555
+ lastError = error;
1556
+ logger.warn(`${operationName} attempt ${attempt} failed:`, error);
1557
+
1558
+ if (attempt < maxRetries) {
1559
+ // Wait before retrying, with exponential backoff
1560
+ const waitTime = delay * Math.pow(2, attempt - 1);
1561
+ logger.debug(`Waiting ${waitTime}ms before retry`);
1562
+ await new Promise(resolve => setTimeout(resolve, waitTime));
1563
+ }
1564
+ }
1565
+ }
1566
+
1567
+ throw new Error(`${operationName} failed after ${maxRetries} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
1568
+ }
1569
+
1570
+ /**
1571
+ * Execute code in the browser context for debugging
1572
+ */
1573
+ async debugEvaluate<T>(code: string | Function): Promise<T> {
1574
+ this.ensureInitialized();
1575
+ if (!this.page) {
1576
+ throw new GeoGebraConnectionError('Page not available');
1577
+ }
1578
+ return await this.page.evaluate(code as any);
1579
+ }
1580
+
1581
+ /**
1582
+ * Update last activity timestamp
1583
+ */
1584
+ private updateActivity(): void {
1585
+ this.lastActivity = new Date();
1586
+ }
1587
+
1588
+ /**
1589
+ * Ensure instance is initialized
1590
+ */
1591
+ private ensureInitialized(): void {
1592
+ if (!this.isInitialized) {
1593
+ throw new GeoGebraConnectionError('GeoGebra instance not initialized');
1594
+ }
1595
+ }
1596
+
1597
+ /**
1598
+ * Animation Methods
1599
+ */
1600
+ async setAnimating(objName: string, animate: boolean): Promise<void> {
1601
+ this.ensureInitialized();
1602
+ this.updateActivity();
1603
+
1604
+ try {
1605
+ await this.page!.evaluate((objName, animate) => {
1606
+ (window as any).ggbApplet.setAnimating(objName, animate);
1607
+ }, objName, animate);
1608
+
1609
+ logger.debug(`Set animating for ${objName} on instance ${this.id}: ${animate}`);
1610
+ } catch (error) {
1611
+ logger.error(`Failed to set animating for ${objName} on instance ${this.id}`, error);
1612
+ throw new GeoGebraError(`Failed to set animating: ${error instanceof Error ? error.message : String(error)}`);
1613
+ }
1614
+ }
1615
+
1616
+ async setAnimationSpeed(objName: string, speed: number): Promise<void> {
1617
+ this.ensureInitialized();
1618
+ this.updateActivity();
1619
+
1620
+ try {
1621
+ await this.page!.evaluate((objName, speed) => {
1622
+ (window as any).ggbApplet.setAnimationSpeed(objName, speed);
1623
+ }, objName, speed);
1624
+
1625
+ logger.debug(`Set animation speed for ${objName} on instance ${this.id}: ${speed}`);
1626
+ } catch (error) {
1627
+ logger.error(`Failed to set animation speed for ${objName} on instance ${this.id}`, error);
1628
+ throw new GeoGebraError(`Failed to set animation speed: ${error instanceof Error ? error.message : String(error)}`);
1629
+ }
1630
+ }
1631
+
1632
+ async startAnimation(): Promise<void> {
1633
+ this.ensureInitialized();
1634
+ this.updateActivity();
1635
+
1636
+ try {
1637
+ await this.page!.evaluate(() => {
1638
+ (window as any).ggbApplet.startAnimation();
1639
+ });
1640
+
1641
+ logger.debug(`Started animation on instance ${this.id}`);
1642
+ } catch (error) {
1643
+ logger.error(`Failed to start animation on instance ${this.id}`, error);
1644
+ throw new GeoGebraError(`Failed to start animation: ${error instanceof Error ? error.message : String(error)}`);
1645
+ }
1646
+ }
1647
+
1648
+ async stopAnimation(): Promise<void> {
1649
+ this.ensureInitialized();
1650
+ this.updateActivity();
1651
+
1652
+ try {
1653
+ await this.page!.evaluate(() => {
1654
+ (window as any).ggbApplet.stopAnimation();
1655
+ });
1656
+
1657
+ logger.debug(`Stopped animation on instance ${this.id}`);
1658
+ } catch (error) {
1659
+ logger.error(`Failed to stop animation on instance ${this.id}`, error);
1660
+ throw new GeoGebraError(`Failed to stop animation: ${error instanceof Error ? error.message : String(error)}`);
1661
+ }
1662
+ }
1663
+
1664
+ async isAnimationRunning(): Promise<boolean> {
1665
+ this.ensureInitialized();
1666
+ this.updateActivity();
1667
+
1668
+ try {
1669
+ const isRunning = await this.page!.evaluate(() => {
1670
+ return (window as any).ggbApplet.isAnimationRunning();
1671
+ }) as boolean;
1672
+
1673
+ logger.debug(`Animation running status on instance ${this.id}: ${isRunning}`);
1674
+ return isRunning;
1675
+ } catch (error) {
1676
+ logger.error(`Failed to check animation status on instance ${this.id}`, error);
1677
+ throw new GeoGebraError(`Failed to check animation status: ${error instanceof Error ? error.message : String(error)}`);
1678
+ }
1679
+ }
1680
+
1681
+ async setTrace(objName: string, flag: boolean): Promise<void> {
1682
+ this.ensureInitialized();
1683
+ this.updateActivity();
1684
+
1685
+ try {
1686
+ await this.page!.evaluate((objName, flag) => {
1687
+ (window as any).ggbApplet.setTrace(objName, flag);
1688
+ }, objName, flag);
1689
+
1690
+ logger.debug(`Set trace for ${objName} on instance ${this.id}: ${flag}`);
1691
+ } catch (error) {
1692
+ logger.error(`Failed to set trace for ${objName} on instance ${this.id}`, error);
1693
+ throw new GeoGebraError(`Failed to set trace: ${error instanceof Error ? error.message : String(error)}`);
1694
+ }
1695
+ }
1696
+
1697
+ async setValue(objName: string, value: number): Promise<void> {
1698
+ this.ensureInitialized();
1699
+ this.updateActivity();
1700
+
1701
+ try {
1702
+ await this.page!.evaluate((objName, value) => {
1703
+ (window as any).ggbApplet.setValue(objName, value);
1704
+ }, objName, value);
1705
+
1706
+ logger.debug(`Set value for ${objName} on instance ${this.id}: ${value}`);
1707
+ } catch (error) {
1708
+ logger.error(`Failed to set value for ${objName} on instance ${this.id}`, error);
1709
+ throw new GeoGebraError(`Failed to set value: ${error instanceof Error ? error.message : String(error)}`);
1710
+ }
1711
+ }
1712
+ }