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