Erste lauffähige Fassung: 3D-Raum, Echtzeit-Multiplayer (WebSocket), Python-IDE via Pyodide im Browser, eigener Arbeitsplatz mit Live-Code-Monitor. Modularer, self-hostbarer Aufbau (Three.js + Node/Express + ws), ohne CDN- Abhängigkeit zur Laufzeit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
4 KiB
JavaScript
110 lines
4 KiB
JavaScript
// 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;
|
|
}
|