// ide.js — 2D-IDE-Overlay über der 3D-Szene: Python-Editor + Ausführung via Pyodide. // // Bewusst als DOM-Overlay (nicht auf eine 3D-Textur gerendert): scharfer Text, freie Maus, // echtes Copy/Paste/Scrollen. Pyodide (CPython als WASM) läuft komplett im Browser — // null Server-Last, keine Sandbox nötig (siehe Spec Abschnitt 7, Variante A). // Editor ist zunächst ein Textarea; Upgrade auf CodeMirror (lokal gehostet) später. import { loadPyodide } from '/vendor/pyodide/pyodide.mjs'; export class IDE { constructor() { this.root = document.getElementById('ide'); this.editor = document.getElementById('ide-editor'); this.output = document.getElementById('ide-output'); this.runBtn = document.getElementById('ide-run'); this.statusEl = document.getElementById('ide-status'); this.isOpen = false; this.pyodide = null; this._loading = null; // Promise, solange Pyodide lädt this._openCb = null; this._closeCb = null; this._changeCb = null; this._changeTimer = null; document.getElementById('ide-close').addEventListener('click', () => this.close()); this.runBtn.addEventListener('click', () => this.run()); this.editor.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); insertText(this.editor, ' '); } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); this.run(); } else if (e.key === 'Escape') this.close(); }); // Live-Änderungen (entprellt) an den Monitor melden. this.editor.addEventListener('input', () => { clearTimeout(this._changeTimer); this._changeTimer = setTimeout(() => { if (this._changeCb) this._changeCb(this.editor.value); }, 250); }); } onOpen(cb) { this._openCb = cb; } onClose(cb) { this._closeCb = cb; } onChange(cb) { this._changeCb = cb; } getCode() { return this.editor.value; } open() { if (this.isOpen) return; this.isOpen = true; this.root.style.display = 'flex'; this.editor.focus(); if (this._openCb) this._openCb(); } close() { if (!this.isOpen) return; this.isOpen = false; this.root.style.display = 'none'; if (this._closeCb) this._closeCb(); } async run() { let py; try { py = await this._ensurePyodide(); } catch (e) { this._status('Python konnte nicht geladen werden', true); this._append('\n[Laden fehlgeschlagen] ' + (e && e.message || e), true); return; } this._clear(); py.setStdout({ batched: (s) => this._append(s) }); py.setStderr({ batched: (s) => this._append(s) }); this._status('läuft …'); try { await py.runPythonAsync(this.editor.value); this._status('fertig ✓'); } catch (e) { this._append('\n' + (e && e.message ? e.message : e), true); this._status('Fehler', true); } } _ensurePyodide() { if (this.pyodide) return Promise.resolve(this.pyodide); if (!this._loading) { this.runBtn.disabled = true; this._status('Python wird geladen … (einmalig, ~10 MB)'); this._loading = loadPyodide({ indexURL: '/vendor/pyodide/' }) .then((py) => { this.pyodide = py; this.runBtn.disabled = false; this._status('bereit'); return py; }) .catch((e) => { this.runBtn.disabled = false; this._loading = null; throw e; }); } return this._loading; } _clear() { this.output.textContent = ''; } _append(text, isErr) { const span = document.createElement('span'); if (isErr) span.className = 'err'; span.textContent = text; this.output.appendChild(span); this.output.scrollTop = this.output.scrollHeight; } _status(text, isErr) { this.statusEl.textContent = text; this.statusEl.classList.toggle('err', !!isErr); } } /** Fügt Text an der Cursorposition eines Textarea ein (für Tab-Einrückung). */ function insertText(ta, text) { const start = ta.selectionStart, end = ta.selectionEnd; ta.value = ta.value.slice(0, start) + text + ta.value.slice(end); ta.selectionStart = ta.selectionEnd = start + text.length; }