@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,108 @@
|
|
|
1
|
+
import { gikzBrowserScripts } from './gikz';
|
|
2
|
+
import { viewerThemes } from './viewer-themes';
|
|
3
|
+
|
|
4
|
+
export interface HtmlOptions {
|
|
5
|
+
/** Kept for API compatibility; the viewer itself intentionally has no title. */
|
|
6
|
+
title?: string;
|
|
7
|
+
commands?: string[];
|
|
8
|
+
ggbBase64?: string;
|
|
9
|
+
/** Initial engine dimensions; the browser viewer resizes to its container. */
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
visibleLabels?: string[];
|
|
13
|
+
captionOverrides?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const scriptJson = (value: unknown) => JSON.stringify(value)
|
|
17
|
+
.replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
18
|
+
|
|
19
|
+
/** A responsive, title-free viewer for an embedded GGB or a semantic command construction. */
|
|
20
|
+
export function constructionHtml(options: HtmlOptions = {}): string {
|
|
21
|
+
const cleaner = gikzBrowserScripts();
|
|
22
|
+
const payload = scriptJson({
|
|
23
|
+
commands: options.commands ?? [],
|
|
24
|
+
...(options.ggbBase64 ? { ggbBase64: options.ggbBase64 } : {}),
|
|
25
|
+
visibleLabels: options.visibleLabels ?? [],
|
|
26
|
+
captionOverrides: options.captionOverrides ?? {}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return `<!doctype html>
|
|
30
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
31
|
+
<meta name="color-scheme" content="light dark"><title>GeoGebra</title>
|
|
32
|
+
<style>
|
|
33
|
+
:root{color-scheme:light;--paper:#fffcf0;--line:#e6e4d9;--ink:#100f0f;--muted:#6f6e69;--hover:#f2f0e5}
|
|
34
|
+
*{box-sizing:border-box}html,body{margin:0;height:100%;overflow:hidden;background:var(--paper);color:var(--ink);font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Noto Sans CJK SC",sans-serif}
|
|
35
|
+
main{height:100dvh;width:100%;padding:12px 16px 16px;display:flex;flex-direction:column}
|
|
36
|
+
header{position:relative;z-index:10;display:flex;justify-content:flex-end;align-items:center;gap:8px;flex-shrink:0;min-height:42px;margin-bottom:8px}
|
|
37
|
+
.menu-control{position:relative}.control-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;appearance:none;height:38px;min-width:72px;padding:0 14px;border:1px solid var(--line);border-radius:9px;background:var(--paper);color:var(--ink);font:inherit;cursor:pointer}
|
|
38
|
+
.control-button:hover,.control-button[aria-expanded="true"]{background:var(--hover)}.control-button:focus-visible,.viewer-menu button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}.icon-button{min-width:38px;width:38px;padding:0}.control-button svg{width:18px;height:18px;flex-shrink:0}.chevron{width:12px!important;height:12px!important}
|
|
39
|
+
.viewer-menu{position:absolute;top:43px;right:0;z-index:20;display:none;min-width:170px;padding:5px;border:1px solid var(--line);border-radius:10px;background:var(--paper);box-shadow:0 12px 32px rgb(16 15 15 / 12%)}
|
|
40
|
+
.viewer-menu.open{display:grid}.viewer-menu button{display:flex;align-items:center;gap:10px;min-height:34px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:var(--ink);text-align:left;font:inherit;cursor:pointer}
|
|
41
|
+
.viewer-menu button:hover,.viewer-menu button[aria-checked="true"]{background:var(--hover)}.viewer-menu{max-height:min(70vh,400px);overflow:auto}.theme-name{flex:1}.swatch{width:12px;height:12px;border-radius:50%;border:1px solid rgb(128 128 128 / 35%)}.sidebar-toggle{position:absolute;top:10px;left:10px;z-index:3;box-shadow:0 2px 8px rgb(0 0 0 / 7%)}.viewer-menu button:disabled{opacity:.5;cursor:wait}
|
|
42
|
+
.stage{position:relative;width:100%;flex:1;min-height:200px;overflow:hidden;border:1px solid var(--line);border-radius:10px;background:var(--paper)}
|
|
43
|
+
#ggb-element{position:absolute;inset:0;width:100%!important;height:100%!important;overflow:hidden}
|
|
44
|
+
#status{position:absolute;inset:0;z-index:2;display:grid;place-items:center;margin:0;padding:16px;color:var(--muted);background:var(--paper);text-align:center;pointer-events:none}
|
|
45
|
+
#status[hidden]{display:none}#status.error{color:#af3029}
|
|
46
|
+
@media(max-width:640px){main{padding:8px}.stage{border-radius:7px}header{margin-bottom:6px}}
|
|
47
|
+
@media print{main{width:100%;padding:0}header,.sidebar-toggle{display:none}.stage{border:0;border-radius:0}}
|
|
48
|
+
</style></head><body><main>
|
|
49
|
+
<header><div class="menu-control"><button id="theme-toggle" class="control-button" type="button" aria-label="选择主题" aria-expanded="false" aria-haspopup="menu" aria-controls="theme-menu"><span id="theme-name">默认</span><svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button><div id="theme-menu" class="viewer-menu" role="menu" aria-label="配色主题"></div></div>
|
|
50
|
+
<button id="mode-toggle" class="control-button icon-button" type="button" aria-label="切换深浅色"></button>
|
|
51
|
+
<div class="menu-control downloads"><button class="control-button download-toggle" id="download-toggle" type="button" aria-expanded="false" aria-haspopup="menu" aria-controls="download-menu">下载</button>
|
|
52
|
+
<div class="viewer-menu" id="download-menu" role="menu" aria-label="下载格式">
|
|
53
|
+
<button type="button" role="menuitem" data-format="ggb">GeoGebra 文件</button>
|
|
54
|
+
<button type="button" role="menuitem" data-format="png">PNG 图片</button>
|
|
55
|
+
<button type="button" role="menuitem" data-format="svg">SVG 矢量图</button>
|
|
56
|
+
<button type="button" role="menuitem" data-format="tikz">TikZ 源文件 (.tex)</button>
|
|
57
|
+
<button type="button" role="menuitem" data-format="pdf">PDF 文档</button>
|
|
58
|
+
<button type="button" role="menuitem" data-format="xml">XML 源文件</button>
|
|
59
|
+
<button type="button" role="menuitem" data-format="html">交互 HTML</button></div></div></header>
|
|
60
|
+
<section class="stage" aria-label="GeoGebra 图形"><button id="sidebar-toggle" class="control-button icon-button sidebar-toggle" type="button" aria-label="展开对象栏" title="展开对象栏" aria-expanded="false" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M9 4v16m4-11 3 3-3 3"/></svg></button><p id="status" role="status" aria-live="polite">正在加载…</p><div id="ggb-element"></div></section></main>
|
|
61
|
+
<script id="construction-data" type="application/json">${payload}</script>
|
|
62
|
+
${cleaner}
|
|
63
|
+
<script>
|
|
64
|
+
(()=>{
|
|
65
|
+
const specification=JSON.parse(document.getElementById('construction-data').textContent);
|
|
66
|
+
const host=document.getElementById('ggb-element'),status=document.getElementById('status');
|
|
67
|
+
const toggle=document.getElementById('download-toggle'),menu=document.getElementById('download-menu');
|
|
68
|
+
const themes=${scriptJson(viewerThemes)};
|
|
69
|
+
const themeMenu=document.getElementById('theme-menu'),themeToggle=document.getElementById('theme-toggle'),modeToggle=document.getElementById('mode-toggle'),sidebarToggle=document.getElementById('sidebar-toggle');
|
|
70
|
+
const themeNames={default:'默认',ayu:'Ayu',catppuccin:'Catppuccin',everforest:'Everforest',gruvbox:'Gruvbox','rose-pine':'Rosé Pine',nord:'Nord',solarized:'Solarized',flexoki:'Flexoki'};
|
|
71
|
+
themeMenu.replaceChildren();
|
|
72
|
+
for(const name of Object.keys(themes)){const button=document.createElement('button');button.type='button';button.dataset.theme=name;button.setAttribute('role','menuitemradio');const label=document.createElement('span');label.className='theme-name';label.textContent=themeNames[name];button.append(label);for(const palette of themes[name]){const swatch=document.createElement('span');swatch.className='swatch';swatch.style.background=palette.paper;swatch.setAttribute('aria-hidden','true');button.append(swatch)}themeMenu.append(button)}
|
|
73
|
+
let api,ready=false,resizeObserver,resizeFrame,resizeVersion=0,sidebarOpen=false,objectThemeApplied=false;
|
|
74
|
+
const themeObjectColors=new Set(Object.values(themes).flat().flatMap(p=>[p.ink,p.blue]).concat(['#000000','#666666','#4d4dff']).map(c=>c.toLowerCase()));
|
|
75
|
+
const validTheme=value=>typeof value==='string'&&/^[a-z-]+:[01]$/.test(value)&&Object.hasOwn(themes,value.split(':')[0]);
|
|
76
|
+
let chosenTheme=validTheme(specification.theme)?specification.theme:'default:0';
|
|
77
|
+
try{const saved=localStorage.getItem('geogebra-viewer-theme');if(validTheme(saved))chosenTheme=saved}catch(_){}
|
|
78
|
+
function applyTheme(){const [name,mode]=chosenTheme.split(':');const palette=themes[name][Number(mode)];for(const [key,value]of Object.entries(palette))document.documentElement.style.setProperty('--'+key,value);document.documentElement.style.colorScheme=Number(mode)?'dark':'light';document.getElementById('theme-name').textContent=themeNames[name];for(const button of themeMenu.querySelectorAll('[data-theme]'))button.setAttribute('aria-checked',String(button.dataset.theme===name));const action=Number(mode)?'切换为浅色':'切换为深色';modeToggle.setAttribute('aria-label',action);modeToggle.title=action;modeToggle.setAttribute('aria-pressed',String(Boolean(Number(mode))));modeToggle.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+(Number(mode)?'<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.4 1.4m11.2 11.2L19 19M5 19l1.4-1.4M17.6 6.4 19 5"/>':'<path d="M20.8 13A9 9 0 0 1 11 3.2 9 9 0 1 0 20.8 13Z"/>')+'</svg>';if(api){const rgb=hex=>[1,3,5].map(i=>parseInt(hex.slice(i,i+2),16));api.setGraphicsOptions(1,{bgColor:palette.paper});for(const object of api.getAllObjectNames()){const type=api.getObjectType(object);if(['point','text','line','segment','ray','circle','conic'].includes(type)&&((!objectThemeApplied&&!specification.ggbBase64)||themeObjectColors.has(api.getColor(object).toLowerCase())))api.setColor(object,...rgb(type==='point'&&api.isMoveable(object)?palette.blue:palette.ink))}objectThemeApplied=true}}
|
|
79
|
+
function saveTheme(){try{localStorage.setItem('geogebra-viewer-theme',chosenTheme)}catch(_){}applyTheme()}
|
|
80
|
+
themeMenu.addEventListener('click',event=>{const button=event.target.closest('[data-theme]');if(!button)return;chosenTheme=button.dataset.theme+':'+chosenTheme.split(':')[1];saveTheme();closeMenus();themeToggle.focus()});
|
|
81
|
+
modeToggle.addEventListener('click',()=>{chosenTheme=chosenTheme.split(':')[0]+':'+(chosenTheme.endsWith(':1')?'0':'1');saveTheme()});applyTheme();
|
|
82
|
+
window.ggbReady=false;window.ggbError=null;
|
|
83
|
+
function setStatus(text,error=false){status.textContent=text;status.classList.toggle('error',error);status.hidden=!text;}
|
|
84
|
+
function equalAxes(){const v=JSON.parse(api.getViewProperties(1)),unit=Math.max(v.invXscale,v.invYscale),cx=v.xMin+v.width*v.invXscale/2,cy=v.yMin+v.height*v.invYscale/2;api.setCoordSystem(cx-v.width*unit/2,cx+v.width*unit/2,cy-v.height*unit/2,cy+v.height*unit/2)}
|
|
85
|
+
function readCamera(){const v=JSON.parse(api.getViewProperties(1));return{cx:v.xMin+v.width*v.invXscale/2,cy:v.yMin+v.height*v.invYscale/2,span:Math.min(v.width*v.invXscale,v.height*v.invYscale)}}
|
|
86
|
+
function setSize(camera){if(!api)return;camera=camera||readCamera();const version=++resizeVersion;cancelAnimationFrame(resizeFrame);resizeFrame=requestAnimationFrame(async()=>{const width=Math.round(host.clientWidth),height=Math.round(host.clientHeight);if(width<1||height<1)return;const {cx,cy,span}=camera;api.setSize(width,height);await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));if(version!==resizeVersion)return;const view=JSON.parse(api.getViewProperties(1)),unit=span/Math.min(view.width,view.height);api.setCoordSystem(cx-view.width*unit/2,cx+view.width*unit/2,cy-view.height*unit/2,cy+view.height*unit/2);equalAxes();sidebarToggle.style.top=Math.max(10,view.top-host.getBoundingClientRect().top+10)+'px';sidebarToggle.style.left=sidebarOpen?Math.max(10,view.left-host.getBoundingClientRect().left+10)+'px':'10px'})}
|
|
87
|
+
function toBytes(data){return Uint8Array.from(atob(data.replace(/^data:[^,]+,/,'')),c=>c.charCodeAt(0));}
|
|
88
|
+
function download(data,mime,extension,base64=false){const blob=new Blob([base64?toBytes(data):data],{type:mime});const url=URL.createObjectURL(blob);const link=document.createElement('a');link.href=url;link.download='construction.'+extension;link.click();setTimeout(()=>URL.revokeObjectURL(url),10000);}
|
|
89
|
+
function callbackExport(format){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('导出超时')),20000);const done=value=>{clearTimeout(timer);value?resolve(value):reject(new Error('GeoGebra 未返回文件'))};try{if(format==='ggb')api.getBase64(done);else if(format==='pdf')api.exportPDF(1,done);else if(format==='tikz')api.exportPGF(done);else api.exportSVG(done)}catch(error){clearTimeout(timer);reject(error)}})}
|
|
90
|
+
async function fitView(){api.showAllObjects();await new Promise(resolve=>setTimeout(resolve,650));const view=JSON.parse(api.getViewProperties(1));let xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity,hasCircle=false,unsupported=false;for(const name of api.getAllObjectNames()){if(!api.getVisible(name))continue;const type=api.getObjectType(name);if(type==='point'){const x=api.getXcoord(name),y=api.getYcoord(name);if(Number.isFinite(x)&&Number.isFinite(y)){xmin=Math.min(xmin,x);xmax=Math.max(xmax,x);ymin=Math.min(ymin,y);ymax=Math.max(ymax,y)}}else if(type==='circle'||type==='conic'){const xml=new DOMParser().parseFromString('<root>'+api.getXML(name)+'</root>','application/xml'),matrix=xml.querySelector('matrix');if(!matrix){unsupported=true;continue}const a=[0,1,2,3,4,5].map(i=>Number(matrix.getAttribute('A'+i)));if(Math.abs(a[0]-a[1])>1e-8||Math.abs(a[3])>1e-8||!a[0]){unsupported=true;continue}const x=-a[4]/a[0],y=-a[5]/a[0],r=Math.sqrt(x*x+y*y-a[2]/a[0]);if(Number.isFinite(r)){hasCircle=true;xmin=Math.min(xmin,x-r);xmax=Math.max(xmax,x+r);ymin=Math.min(ymin,y-r);ymax=Math.max(ymax,y+r)}}else if(!['line','ray','segment','text','numeric','boolean'].includes(type))unsupported=true}if(!hasCircle||unsupported){xmin=view.xMin;xmax=xmin+view.width*view.invXscale;ymin=view.yMin;ymax=ymin+view.height*view.invYscale}const unit=Math.max((xmax-xmin)*1.16/view.width,(ymax-ymin)*1.16/view.height,2/Math.min(view.width,view.height)),cx=(xmin+xmax)/2,cy=(ymin+ymax)/2;api.setCoordSystem(cx-view.width*unit/2,cx+view.width*unit/2,cy-view.height*unit/2,cy+view.height*unit/2)}
|
|
91
|
+
|
|
92
|
+
function applyMcpDefaults(){const names=api.getAllObjectNames();const visible=new Set(specification.visibleLabels||[]),captions=specification.captionOverrides||{};const view=JSON.parse(api.getViewProperties(1)),dx=10*view.invXscale,dy=10*view.invYscale;for(const name of names){try{const type=api.getObjectType(name),show=visible.size?visible.has(name):type==='point';api.setLabelVisible(name,false);if(type==='point')api.setPointSize(name,3);else if(['line','segment','ray','vector','circle','conic','polygon','function','curve'].includes(type))api.setLineThickness(name,3);if(show){let caption=captions[name]||name;caption=caption.replace(/^\\$|\\$$/g,'');const math=caption==='Γ'?'\\\\Gamma':caption==='ω'?'\\\\omega':/^[A-Za-z]+$/.test(caption)?'\\\\mathit{'+caption+'}':caption;const anchor=type==='point'?name:'Point('+name+',0.38)';const label='mcpLabel'+name;api.evalCommand(label+' = Text('+JSON.stringify(math).replaceAll(String.fromCharCode(92,92),String.fromCharCode(92))+', '+anchor+' + ('+dx+','+dy+'), true, true)');api.setLabelVisible(label,false);api.setFixed(label,true,false);api.setAuxiliary(label,true)}}catch(_){}}}
|
|
93
|
+
|
|
94
|
+
async function exportAs(format){if(!ready)return;try{if(format==='tikz'){if(typeof api.exportPGF!=='function')throw new Error('当前 GeoGebra 引擎不支持 TikZ 导出');setStatus('正在通过 Gikz 清洗…');const source=await callbackExport('tikz');if(!globalThis.Gikz)throw new Error('Gikz 未加载,已停止 TikZ 导出');const cleaned=Gikz.wrapStandalone(Gikz.cleanTikZCode(source));download(cleaned,'application/x-tex','tex')}else if(format==='xml'){download(api.getXML(),'application/xml','xml')}else if(format==='png'){download(api.getPNGBase64(2,false,144),'image/png','png',true)}else if(format==='html'){const base64=await callbackExport('ggb');const html=window.constructionTemplate.replace(/(<script id="construction-data" type="application\\/json">)[\\s\\S]*?(<\\/script>)/,'$1'+JSON.stringify({commands:[],ggbBase64:base64,theme:chosenTheme,visibleLabels:specification.visibleLabels||[],captionOverrides:specification.captionOverrides||{}})+'$2');download(html,'text/html','html')}else{const value=await callbackExport(format);download(value,format==='pdf'?'application/pdf':format==='ggb'?'application/vnd.geogebra.file':'image/svg+xml',format,format==='pdf'||format==='ggb')}setStatus('')}catch(error){setStatus(error.message||'导出失败',true)}}
|
|
95
|
+
const popupMenus=[[toggle,menu],[themeToggle,themeMenu]];
|
|
96
|
+
function closeMenus(){for(const [button,popup]of popupMenus){popup.classList.remove('open');button.setAttribute('aria-expanded','false')}}
|
|
97
|
+
for(const [button,popup]of popupMenus){button.addEventListener('click',()=>{const open=!popup.classList.contains('open');closeMenus();if(open){popup.classList.add('open');button.setAttribute('aria-expanded','true')}});button.addEventListener('keydown',event=>{if(event.key==='ArrowDown'){event.preventDefault();closeMenus();popup.classList.add('open');button.setAttribute('aria-expanded','true');popup.querySelector('button').focus()}});popup.addEventListener('keydown',event=>{const items=[...popup.querySelectorAll('button')],index=items.indexOf(document.activeElement);if(['ArrowDown','ArrowUp','Home','End'].includes(event.key)){event.preventDefault();items[event.key==='Home'?0:event.key==='End'?items.length-1:(index+(event.key==='ArrowDown'?1:items.length-1))%items.length].focus()}if(event.key==='Escape'){closeMenus();button.focus()}})}
|
|
98
|
+
menu.addEventListener('click',async event=>{const button=event.target.closest('[data-format]');if(!button)return;closeMenus();await exportAs(button.dataset.format)});
|
|
99
|
+
document.addEventListener('click',event=>{if(!event.target.closest('.menu-control'))closeMenus()});
|
|
100
|
+
document.addEventListener('keydown',event=>{if(event.key==='Escape')closeMenus()});
|
|
101
|
+
sidebarToggle.addEventListener('click',()=>{if(!ready)return;const camera=readCamera();sidebarOpen=!sidebarOpen;api.setPerspective(sidebarOpen?'AG':'G');api.showAlgebraInput(sidebarOpen);api.showToolBar(sidebarOpen);api.showMenuBar(sidebarOpen);sidebarToggle.setAttribute('aria-expanded',String(sidebarOpen));sidebarToggle.setAttribute('aria-label',sidebarOpen?'收起对象栏':'展开对象栏');sidebarToggle.title=sidebarOpen?'收起对象栏':'展开对象栏';sidebarToggle.querySelector('svg path').setAttribute('d',sidebarOpen?'M9 4v16m7-11-3 3 3 3':'M9 4v16m4-11 3 3-3 3');setSize(camera)});
|
|
102
|
+
// Initialize the native editing UI, then collapse it on load; starting with it disabled leaves lazy editor actions uninitialized.
|
|
103
|
+
window.startGeoGebra=()=>{window.constructionTemplate='<!doctype html>\\n'+document.documentElement.outerHTML;const width=Math.max(200,Math.round(host.clientWidth)),height=Math.max(200,Math.round(host.clientHeight));const applet=new GGBApplet({appName:'classic',width,height,disableAutoScale:true,showZoomButtons:false,borderColor:'transparent',language:'zh-CN',showToolBar:true,showMenuBar:true,showAlgebraInput:true,allowStyleBar:true,enableRightClick:true,showResetIcon:false,useBrowserForJS:false,preventFocus:true,enableLabelDrags:true,enableShiftDragZoom:true,...(specification.ggbBase64?{ggbBase64:specification.ggbBase64}:{}),appletOnLoad:async loaded=>{api=loaded;window.ggbApplet=api;try{api.setErrorDialogsActive(false);if(!specification.ggbBase64||(specification.commands||[]).length){api.setPerspective('G');api.setAxesVisible(false,false);api.setGridVisible(false);for(const command of specification.commands||[]){if(!api.evalCommand(command))throw new Error('GeoGebra 无法识别命令:'+command)}if((specification.commands||[]).length)await fitView();applyMcpDefaults()}api.setPerspective('G');api.showAlgebraInput(false);api.showToolBar(false);api.showMenuBar(false);api.setErrorDialogsActive(true);api.setAxesVisible(false,false);equalAxes();applyTheme();ready=true;sidebarToggle.disabled=false;window.ggbReady=true;status.hidden=true;resizeObserver=new ResizeObserver(()=>setSize());resizeObserver.observe(document.querySelector('.stage'));setSize()}catch(error){window.ggbError=error.message;setStatus(error.message,true)}}},true);applet.inject('ggb-element')};
|
|
104
|
+
setTimeout(()=>{if(!ready&&!window.ggbError){window.ggbError='GeoGebra 加载超时,请检查网络后刷新';setStatus(window.ggbError,true)}},30000);
|
|
105
|
+
})();
|
|
106
|
+
</script><script src="https://www.geogebra.org/apps/deployggb.js" onload="startGeoGebra()" onerror="window.ggbError='GeoGebra 加载失败';document.getElementById('status').className='error';document.getElementById('status').textContent=window.ggbError"></script>
|
|
107
|
+
</body></html>`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { XMLParser, XMLValidator } from 'fast-xml-parser';
|
|
2
|
+
|
|
3
|
+
export interface SceneObject {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
visible: boolean;
|
|
7
|
+
labelVisible: boolean;
|
|
8
|
+
caption: string;
|
|
9
|
+
coordinates?: [number, number];
|
|
10
|
+
command?: { name: string; inputs: string[]; outputs: string[] };
|
|
11
|
+
expression?: string;
|
|
12
|
+
style: { color: [number, number, number]; thickness: number; dash: number; pointSize: number; pointStyle: number; fill: number };
|
|
13
|
+
}
|
|
14
|
+
export interface SemanticScene { schemaVersion: '1.0'; source: 'geogebra-xml'; xml: string; objects: SceneObject[]; }
|
|
15
|
+
const array = (value: any): any[] => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
16
|
+
const inputs = (value: Record<string, string> = {}) => Object.keys(value).filter(k => /^a\d+$/.test(k)).sort((a,b) => Number(a.slice(1))-Number(b.slice(1))).map(k => value[k]!);
|
|
17
|
+
const number = (v: unknown, fallback: number) => Number.isFinite(Number(v)) ? Number(v) : fallback;
|
|
18
|
+
|
|
19
|
+
/** Preserve command edges as well as evaluated coordinates and appearance. */
|
|
20
|
+
export function sceneFromXML(xml: string): SemanticScene {
|
|
21
|
+
if (xml.length > 20_000_000 || /<!DOCTYPE|<!ENTITY/i.test(xml)) throw new Error('XML is too large or contains a forbidden document declaration');
|
|
22
|
+
if (XMLValidator.validate(xml) !== true) throw new Error('Malformed GeoGebra XML');
|
|
23
|
+
const doc = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false }).parse(xml);
|
|
24
|
+
const construction = doc.geogebra?.construction;
|
|
25
|
+
if (!construction) throw new Error('Expected GeoGebra construction XML');
|
|
26
|
+
const commands = new Map<string, SceneObject['command']>();
|
|
27
|
+
for (const c of array(construction.command)) {
|
|
28
|
+
const command = { name: String(c.name), inputs: inputs(c.input), outputs: inputs(c.output) };
|
|
29
|
+
for (const output of command.outputs) commands.set(output, command);
|
|
30
|
+
}
|
|
31
|
+
const expressions = new Map(array(construction.expression).map(e => [String(e.label), String(e.exp)]));
|
|
32
|
+
const objects: SceneObject[] = array(construction.element).map(e => {
|
|
33
|
+
const obj: SceneObject = {
|
|
34
|
+
name: String(e.label), type: String(e.type), visible: e.show?.object !== 'false',
|
|
35
|
+
labelVisible: e.show?.label === 'true', caption: String(e.caption?.val ?? e.label),
|
|
36
|
+
style: {
|
|
37
|
+
color: [number(e.objColor?.r,0),number(e.objColor?.g,0),number(e.objColor?.b,0)],
|
|
38
|
+
thickness: number(e.lineStyle?.thickness,2), dash: number(e.lineStyle?.type,0),
|
|
39
|
+
pointSize: number(e.pointSize?.val,2), pointStyle: number(e.pointStyle?.val,0), fill: number(e.objColor?.alpha,0),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const command = commands.get(obj.name);
|
|
43
|
+
if (command) obj.command = command;
|
|
44
|
+
const expression = expressions.get(obj.name);
|
|
45
|
+
if (expression) obj.expression = expression;
|
|
46
|
+
if (e.coords && obj.type === 'point') {
|
|
47
|
+
const z = number(e.coords.z,1);
|
|
48
|
+
const x = Number(e.coords.x)/z, y = Number(e.coords.y)/z;
|
|
49
|
+
if (Number.isFinite(x) && Number.isFinite(y)) obj.coordinates = [x,y];
|
|
50
|
+
}
|
|
51
|
+
return obj;
|
|
52
|
+
});
|
|
53
|
+
return { schemaVersion: '1.0', source: 'geogebra-xml', xml, objects };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type SourceFormat = 'tikz' | 'asymptote' | 'tsqx';
|
|
57
|
+
export interface SourceResult {
|
|
58
|
+
format: SourceFormat;
|
|
59
|
+
code: string;
|
|
60
|
+
complete: boolean;
|
|
61
|
+
warnings: string[];
|
|
62
|
+
coordinateFallbacks: string[];
|
|
63
|
+
unsupportedObjects: string[];
|
|
64
|
+
semantics: 'preserved-subset' | 'partial';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Deliberately small semantic backend. Unsupported relations remain visible in the report. */
|
|
68
|
+
export function exportSource(scene: SemanticScene, format: SourceFormat, strict = true, standalone = true): SourceResult {
|
|
69
|
+
const warnings: string[] = [], coordinateFallbacks: string[] = [], unsupportedObjects: string[] = [];
|
|
70
|
+
const definitions: string[] = [], drawings: string[] = [], markers: string[] = [];
|
|
71
|
+
const names = new Map(scene.objects.map((o,i) => [o.name, `g${i}`]));
|
|
72
|
+
const byName = new Map(scene.objects.map(o => [o.name,o]));
|
|
73
|
+
const emitted = new Set<string>(), visiting = new Set<string>();
|
|
74
|
+
const id = (s: string) => names.get(s)!;
|
|
75
|
+
const n = (x: number) => Number(x.toPrecision(12)).toString();
|
|
76
|
+
const fail = (o: SceneObject, why: string) => { unsupportedObjects.push(o.name); warnings.push(`${o.name}: ${why}`); };
|
|
77
|
+
const point = (name: string): boolean => byName.get(name)?.type === 'point' && emit(byName.get(name)!);
|
|
78
|
+
const asyColor = (o: SceneObject) => `rgb(${o.style.color.map(c=>n(c/255)).join(',')})`;
|
|
79
|
+
const pen = (o: SceneObject) => `${asyColor(o)}+linewidth(${n(o.style.thickness/2)}bp)${o.style.dash ? '+dashed' : ''}`;
|
|
80
|
+
const tikzStyle = (o: SceneObject) => `draw={rgb,255:red,${o.style.color[0]};green,${o.style.color[1]};blue,${o.style.color[2]}},line width=${n(o.style.thickness/2)}pt${o.style.dash ? ',dashed' : ''}`;
|
|
81
|
+
const raw = (s: string) => format === 'tsqx' ? `!${s}` : s;
|
|
82
|
+
|
|
83
|
+
function emit(o: SceneObject): boolean {
|
|
84
|
+
if (emitted.has(o.name)) return true;
|
|
85
|
+
if (visiting.has(o.name)) { fail(o,'cyclic dependency'); return false; }
|
|
86
|
+
visiting.add(o.name);
|
|
87
|
+
const key = id(o.name), c = o.command, args = c?.inputs ?? [];
|
|
88
|
+
if (o.type === 'point') {
|
|
89
|
+
let expr: string | undefined;
|
|
90
|
+
if (c?.name === 'Midpoint' && args.length === 2 && args.every(point)) {
|
|
91
|
+
expr = format === 'tsqx' ? `midpoint ${id(args[0]!)} ${id(args[1]!)}` : format === 'tikz' ? `($(${id(args[0]!)})!0.5!(${id(args[1]!)})$)` : `(${id(args[0]!)}+${id(args[1]!)})/2`;
|
|
92
|
+
} else if (!c && o.coordinates) {
|
|
93
|
+
expr = `(${o.coordinates.map(n).join(',')})`;
|
|
94
|
+
if (o.expression && !/^\(\s*[-+\d.eE]+\s*,\s*[-+\d.eE]+\s*\)$/.test(o.expression)) {
|
|
95
|
+
warnings.push(`${o.name}: original algebraic expression is retained in XML; source uses evaluated coordinates`);
|
|
96
|
+
coordinateFallbacks.push(o.name);
|
|
97
|
+
}
|
|
98
|
+
} else if (o.coordinates && !strict) {
|
|
99
|
+
expr = `(${o.coordinates.map(n).join(',')})`;
|
|
100
|
+
coordinateFallbacks.push(o.name);
|
|
101
|
+
warnings.push(`${o.name}: ${c?.name ?? 'undefined point'} frozen to evaluated coordinates`);
|
|
102
|
+
}
|
|
103
|
+
if (!expr) { fail(o,`unsupported or undefined point definition ${c?.name ?? ''}`); visiting.delete(o.name); return false; }
|
|
104
|
+
definitions.push(format === 'tikz' ? `\\coordinate (${key}) at ${expr};` : format === 'tsqx' ? `${key} : = ${expr}` : `pair ${key}=${expr};`);
|
|
105
|
+
if (o.visible) {
|
|
106
|
+
if (o.style.pointStyle !== 0) warnings.push(`${o.name}: point marker normalized to filled dot`);
|
|
107
|
+
if (format === 'tikz') markers.push(`\\fill[${tikzStyle(o).replace('draw=','fill=')}] (${key}) circle (${n(o.style.pointSize/2)}pt);`);
|
|
108
|
+
else markers.push(raw(`dot(${key},${asyColor(o)}+linewidth(${n(o.style.pointSize/3)}bp));`));
|
|
109
|
+
if (o.labelVisible) {
|
|
110
|
+
const caption = o.caption.replace(/^\$|\$$/g,'');
|
|
111
|
+
if (format === 'tikz') markers.push(`\\node[above right] at (${key}) {$${caption}$};`);
|
|
112
|
+
else markers.push(raw(`label(${JSON.stringify('$'+caption+'$')},${key},NE,${asyColor(o)});`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else if (c && ['Segment','Polygon','Circle'].includes(c.name) && args.every(point)) {
|
|
116
|
+
const refs = args.map(id);
|
|
117
|
+
if (c.name === 'Segment' && refs.length === 2) {
|
|
118
|
+
if (o.visible) drawings.push(format === 'tikz' ? `\\draw[${tikzStyle(o)}] (${refs[0]})--(${refs[1]});` : raw(`draw(${refs.join('--')},${pen(o)});`));
|
|
119
|
+
} else if (c.name === 'Polygon' && refs.length >= 3 && c.outputs[0] === o.name) {
|
|
120
|
+
if (o.visible) {
|
|
121
|
+
const route = format === 'tikz' ? refs.map(r=>`(${r})`).join('--')+'--cycle' : refs.join('--')+'--cycle';
|
|
122
|
+
drawings.push(format === 'tikz' ? `\\path[${tikzStyle(o)},fill={rgb,255:red,${o.style.color[0]};green,${o.style.color[1]};blue,${o.style.color[2]}},fill opacity=${n(o.style.fill)}] ${route};` : raw(o.style.fill > 0 ? `filldraw(${route},${asyColor(o)}+opacity(${n(o.style.fill)}),${pen(o)});` : `draw(${route},${pen(o)});`));
|
|
123
|
+
}
|
|
124
|
+
} else if (c.name === 'Circle' && refs.length === 3) {
|
|
125
|
+
const positions = args.map(a=>byName.get(a)?.coordinates);
|
|
126
|
+
if (positions.every(p=>p!==undefined)) {
|
|
127
|
+
const [a,b,d] = positions as [number,number][];
|
|
128
|
+
const determinant=(b![0]-a![0])*(d![1]-a![1])-(b![1]-a![1])*(d![0]-a![0]);
|
|
129
|
+
if (determinant === 0) { fail(o,'three-point circle is degenerate'); visiting.delete(o.name); return false; }
|
|
130
|
+
}
|
|
131
|
+
const center = `${key}center`;
|
|
132
|
+
if (format === 'tikz') {
|
|
133
|
+
// Work in centimetres to keep PGF's fixed-point intermediates small.
|
|
134
|
+
const x = (i: number) => `(\\x${i}/1cm)`, y = (i: number) => `(\\y${i}/1cm)`;
|
|
135
|
+
const q = (i: number) => `(${x(i)}*${x(i)}+${y(i)}*${y(i)})`;
|
|
136
|
+
const denominator = `(2*(${x(1)}*(${y(2)}-${y(3)})+${x(2)}*(${y(3)}-${y(1)})+${x(3)}*(${y(1)}-${y(2)})))`;
|
|
137
|
+
const cx = `(${q(1)}*(${y(2)}-${y(3)})+${q(2)}*(${y(3)}-${y(1)})+${q(3)}*(${y(1)}-${y(2)}))/${denominator}`;
|
|
138
|
+
const cy = `(${q(1)}*(${x(3)}-${x(2)})+${q(2)}*(${x(1)}-${x(3)})+${q(3)}*(${x(2)}-${x(1)}))/${denominator}`;
|
|
139
|
+
definitions.push(`\\path let \\p1=(${refs[0]}),\\p2=(${refs[1]}),\\p3=(${refs[2]}) in coordinate (${center}) at ({(${cx})*1cm},{(${cy})*1cm});`);
|
|
140
|
+
if (o.visible) drawings.push(`\\draw[${tikzStyle(o)}] let \\p1=($(${refs[0]})-(${center})$) in (${center}) circle ({veclen(\\x1,\\y1)});`);
|
|
141
|
+
} else {
|
|
142
|
+
definitions.push(raw(`pair ${center}=ggbCircumcenter(${refs.join(',')});`));
|
|
143
|
+
if (o.visible) drawings.push(raw(`draw(circle(${center},abs(${refs[0]}-${center})),${pen(o)});`));
|
|
144
|
+
}
|
|
145
|
+
} else if (c.name === 'Circle' && refs.length === 2) {
|
|
146
|
+
if (o.visible) drawings.push(format === 'tikz' ? `\\draw[${tikzStyle(o)}] let \\p1=($(${refs[1]})-(${refs[0]})$) in (${refs[0]}) circle ({veclen(\\x1,\\y1)});` : raw(`draw(circle(${refs[0]},abs(${refs[1]}-${refs[0]})),${pen(o)});`));
|
|
147
|
+
} else { fail(o,`unsupported ${c.name} overload`); visiting.delete(o.name); return false; }
|
|
148
|
+
if (o.labelVisible) warnings.push(`${o.name}: non-point label retained in XML but not drawn`);
|
|
149
|
+
if (o.style.fill && c.name === 'Circle') warnings.push(`${o.name}: circle fill omitted`);
|
|
150
|
+
} else {
|
|
151
|
+
// Polygon side outputs have their own segment elements but share Polygon's command.
|
|
152
|
+
if (o.type === 'segment' && c?.name === 'Polygon' && c.outputs.indexOf(o.name)>0) {
|
|
153
|
+
const side=c.outputs.indexOf(o.name)-1, a=args[side], b=args[(side+1)%args.length];
|
|
154
|
+
if (a && b) {
|
|
155
|
+
const replacement: SceneObject = {...o, command:{name:'Segment',inputs:[a,b],outputs:[o.name]}};
|
|
156
|
+
visiting.delete(o.name); return emit(replacement);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
fail(o,`unsupported ${o.type} / ${c?.name ?? 'expression'}`); visiting.delete(o.name); return false;
|
|
160
|
+
}
|
|
161
|
+
visiting.delete(o.name); emitted.add(o.name); return true;
|
|
162
|
+
}
|
|
163
|
+
for (const o of scene.objects.filter(o=>o.visible)) emit(o);
|
|
164
|
+
if (unsupportedObjects.length || coordinateFallbacks.length) warnings.push('Use the original XML and geometry-export skill to reconstruct the remaining relations.');
|
|
165
|
+
warnings.push('GeoGebra marker and stroke sizes are approximated in TeX points; verify the print scale.');
|
|
166
|
+
if (scene.objects.some(o=>o.visible&&o.labelVisible)) warnings.push('Label positions use a default offset; review against the GeoGebra rendering.');
|
|
167
|
+
if (scene.objects.some(o=>o.visible&&o.style.dash)) warnings.push('Non-solid line patterns are normalized to dashed.');
|
|
168
|
+
let code: string;
|
|
169
|
+
if (format === 'tikz') {
|
|
170
|
+
const fragment = ['\\begin{tikzpicture}[line cap=round,line join=round]',...definitions,...drawings,...markers,'\\end{tikzpicture}'].join('\n');
|
|
171
|
+
code = standalone ? `\\documentclass[tikz,border=3pt]{standalone}\n\\usetikzlibrary{calc}\n\\begin{document}\n${fragment}\n\\end{document}\n` : fragment;
|
|
172
|
+
} else {
|
|
173
|
+
const header = format === 'tsqx' ? ['# Mixed TSQX/Asymptote: native point definitions, explicit style/label drawing', '!import graph;', '!size(10cm);','!pair midpoint(pair a,pair b){ return (a+b)/2; }'] : ['import graph;','size(10cm);'];
|
|
174
|
+
const helper = raw('pair ggbCircumcenter(pair a,pair b,pair c){ real d=2*(a.x*(b.y-c.y)+b.x*(c.y-a.y)+c.x*(a.y-b.y)); return ((dot(a,a)*(b.y-c.y)+dot(b,b)*(c.y-a.y)+dot(c,c)*(a.y-b.y))/d,(dot(a,a)*(c.x-b.x)+dot(b,b)*(a.x-c.x)+dot(c,c)*(b.x-a.x))/d); }');
|
|
175
|
+
code = [...header,helper,...definitions,...drawings,...markers].join('\n')+'\n';
|
|
176
|
+
}
|
|
177
|
+
const complete = unsupportedObjects.length===0 && coordinateFallbacks.length===0;
|
|
178
|
+
if (strict && !complete) throw new Error(`Semantic export requires review: ${warnings.join('; ')}. Use strict=false for an explicitly partial draft.`);
|
|
179
|
+
return { format,code,complete,warnings,coordinateFallbacks,unsupportedObjects,semantics:complete?'preserved-subset':'partial' };
|
|
180
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/** Reader palette tokens from obsidianmd/obsidian-clipper (MIT). See THIRD_PARTY_NOTICES.md. */
|
|
2
|
+
export const viewerThemes = {
|
|
3
|
+
"default": [
|
|
4
|
+
{
|
|
5
|
+
"paper": "#ffffff",
|
|
6
|
+
"hover": "#fcfcfc",
|
|
7
|
+
"ink": "#111111",
|
|
8
|
+
"muted": "#5c5c5c",
|
|
9
|
+
"line": "#e0e0e0",
|
|
10
|
+
"blue": "#2166F6"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"paper": "#1e1e1e",
|
|
14
|
+
"hover": "#212121",
|
|
15
|
+
"ink": "#dadada",
|
|
16
|
+
"muted": "#b3b3b3",
|
|
17
|
+
"line": "#363636",
|
|
18
|
+
"blue": "#8CAAEE"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"ayu": [
|
|
22
|
+
{
|
|
23
|
+
"paper": "#fcfcfc",
|
|
24
|
+
"hover": "#f8f9fa",
|
|
25
|
+
"ink": "#5c6166",
|
|
26
|
+
"muted": "#8a9199",
|
|
27
|
+
"line": "#6b7d8f1f",
|
|
28
|
+
"blue": "#22a4e6"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"paper": "#1f2430",
|
|
32
|
+
"hover": "#242936",
|
|
33
|
+
"ink": "#cccac2",
|
|
34
|
+
"muted": "#707a8c",
|
|
35
|
+
"line": "#282e3b",
|
|
36
|
+
"blue": "#73d0ff"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"catppuccin": [
|
|
40
|
+
{
|
|
41
|
+
"paper": "#eff1f5",
|
|
42
|
+
"hover": "#e6e9ef",
|
|
43
|
+
"ink": "#4c4f69",
|
|
44
|
+
"muted": "#8c8fa1",
|
|
45
|
+
"line": "#dce0e8",
|
|
46
|
+
"blue": "#2166F6"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"paper": "#242634",
|
|
50
|
+
"hover": "#303446",
|
|
51
|
+
"ink": "#c6d0f5",
|
|
52
|
+
"muted": "#A6ADCE",
|
|
53
|
+
"line": "#41455A",
|
|
54
|
+
"blue": "#8CAAEE"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"everforest": [
|
|
58
|
+
{
|
|
59
|
+
"paper": "#fdf6e3",
|
|
60
|
+
"hover": "#f4f0d9",
|
|
61
|
+
"ink": "#5c6a72",
|
|
62
|
+
"muted": "#829181",
|
|
63
|
+
"line": "#e6e2cc",
|
|
64
|
+
"blue": "#3795C5"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"paper": "#2d353b",
|
|
68
|
+
"hover": "#343f44",
|
|
69
|
+
"ink": "#d3c6aa",
|
|
70
|
+
"muted": "#9da9a0",
|
|
71
|
+
"line": "#475258",
|
|
72
|
+
"blue": "#7fbbb3"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"gruvbox": [
|
|
76
|
+
{
|
|
77
|
+
"paper": "#fbf1c7",
|
|
78
|
+
"hover": "#f2e5bc",
|
|
79
|
+
"ink": "#3c3836",
|
|
80
|
+
"muted": "#7c7065",
|
|
81
|
+
"line": "#e0d09a",
|
|
82
|
+
"blue": "#458588"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"paper": "#282828",
|
|
86
|
+
"hover": "#3c3836",
|
|
87
|
+
"ink": "#ebdbb2",
|
|
88
|
+
"muted": "#bdae93",
|
|
89
|
+
"line": "#504945",
|
|
90
|
+
"blue": "#83a598"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"rose-pine": [
|
|
94
|
+
{
|
|
95
|
+
"paper": "#fffaf3",
|
|
96
|
+
"hover": "#faf4ed",
|
|
97
|
+
"ink": "#575279",
|
|
98
|
+
"muted": "#9893a5",
|
|
99
|
+
"line": "#f4ede8",
|
|
100
|
+
"blue": "#286983"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"paper": "#191724",
|
|
104
|
+
"hover": "#1f1d2e",
|
|
105
|
+
"ink": "#e0def4",
|
|
106
|
+
"muted": "#6e6a86",
|
|
107
|
+
"line": "#21202e",
|
|
108
|
+
"blue": "#3e8fb0"
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"nord": [
|
|
112
|
+
{
|
|
113
|
+
"paper": "#eceff4",
|
|
114
|
+
"hover": "#e5e9f0",
|
|
115
|
+
"ink": "#2e3440",
|
|
116
|
+
"muted": "#8a9199",
|
|
117
|
+
"line": "#d8dee9",
|
|
118
|
+
"blue": "#81A1C1"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"paper": "#3b4252",
|
|
122
|
+
"hover": "#434c5e",
|
|
123
|
+
"ink": "#d8dee9",
|
|
124
|
+
"muted": "#7b88a1",
|
|
125
|
+
"line": "#4c566a",
|
|
126
|
+
"blue": "#81A1C1"
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"solarized": [
|
|
130
|
+
{
|
|
131
|
+
"paper": "#fdf6e3",
|
|
132
|
+
"hover": "#eee8d5",
|
|
133
|
+
"ink": "#073642",
|
|
134
|
+
"muted": "#586e75",
|
|
135
|
+
"line": "#e9e1c8",
|
|
136
|
+
"blue": "#268bd2"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"paper": "#002b36",
|
|
140
|
+
"hover": "#073642",
|
|
141
|
+
"ink": "#839496",
|
|
142
|
+
"muted": "#586e75",
|
|
143
|
+
"line": "#19414B",
|
|
144
|
+
"blue": "#268bd2"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"flexoki": [
|
|
148
|
+
{
|
|
149
|
+
"paper": "#FFFCF0",
|
|
150
|
+
"hover": "#F2F0E5",
|
|
151
|
+
"ink": "#100F0F",
|
|
152
|
+
"muted": "#5c5c5c",
|
|
153
|
+
"line": "#E6E4D9",
|
|
154
|
+
"blue": "#205EA6"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"paper": "#100F0F",
|
|
158
|
+
"hover": "#1C1B1A",
|
|
159
|
+
"ink": "#CECDC3",
|
|
160
|
+
"muted": "#878580",
|
|
161
|
+
"line": "#282726",
|
|
162
|
+
"blue": "#4385BE"
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { realpath, stat } from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const context=new AsyncLocalStorage<string>();
|
|
6
|
+
export const currentWorkspace=()=>context.getStore()||process.env['VSCODE_WORKSPACE_FOLDER']||process.cwd();
|
|
7
|
+
|
|
8
|
+
/** Keep the requested workspace scoped to one MCP call, including asynchronous exports. */
|
|
9
|
+
export async function inWorkspace<T>(workspace:unknown,operation:()=>Promise<T>):Promise<T>{
|
|
10
|
+
if(workspace===undefined)return operation();
|
|
11
|
+
if(typeof workspace!=='string'||!path.isAbsolute(workspace))throw new Error('workspace must be an absolute directory path');
|
|
12
|
+
const root=await realpath(workspace);
|
|
13
|
+
if(!(await stat(root)).isDirectory())throw new Error('workspace must be a directory');
|
|
14
|
+
return context.run(root,operation);
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { McpServer } from './server';
|
|
3
|
+
import { McpServerConfig } from './types/mcp';
|
|
4
|
+
import logger from './utils/logger';
|
|
5
|
+
import { getAppInfo } from './utils/app-info';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Main entry point for the GeoGebra Studio server
|
|
9
|
+
*/
|
|
10
|
+
async function main() {
|
|
11
|
+
try {
|
|
12
|
+
const appInfo = getAppInfo();
|
|
13
|
+
|
|
14
|
+
// Server configuration
|
|
15
|
+
const config: McpServerConfig = {
|
|
16
|
+
name: 'GeoGebra Studio',
|
|
17
|
+
version: appInfo.version,
|
|
18
|
+
description: appInfo.description,
|
|
19
|
+
logLevel: (process.env['LOG_LEVEL'] as 'error' | 'warn' | 'info' | 'debug') || 'info'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Create and start the MCP server
|
|
23
|
+
const server = new McpServer(config);
|
|
24
|
+
|
|
25
|
+
// Set up graceful shutdown
|
|
26
|
+
const shutdown = async (signal: string) => {
|
|
27
|
+
logger.info(`Received ${signal}, shutting down gracefully...`);
|
|
28
|
+
try {
|
|
29
|
+
await server.stop();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error('Error during shutdown:', error);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Handle shutdown signals
|
|
38
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
39
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
40
|
+
|
|
41
|
+
// Handle uncaught exceptions
|
|
42
|
+
process.on('uncaughtException', (error) => {
|
|
43
|
+
logger.error('Uncaught exception:', error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
48
|
+
logger.error('Unhandled rejection at:', promise, 'reason:', reason);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Start the server
|
|
53
|
+
await server.start();
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error('Failed to start server:', error);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Start the application
|
|
62
|
+
if (require.main === module) {
|
|
63
|
+
main().catch((error) => {
|
|
64
|
+
logger.error('Application startup failed:', error);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { McpServer } from './server';
|
|
70
|
+
export { toolRegistry } from './tools';
|
|
71
|
+
export * from './types/mcp';
|