aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-07-02 06:00:40 +0200
committerAgando <roduccomm@gmail.com>2026-07-02 06:00:40 +0200
commitea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (patch)
tree2361e33e52cdb4e0880ed0c776ed75943b37d7cf /modules/ext
parent8b3e3e4527772bf060984f98777a2cff8e88aa09 (diff)
auto: 2026-07-02_06:00 changes
Diffstat (limited to 'modules/ext')
-rw-r--r--modules/ext/mindmap.js65
-rw-r--r--modules/ext/three.js1176
2 files changed, 1240 insertions, 1 deletions
diff --git a/modules/ext/mindmap.js b/modules/ext/mindmap.js
index a5e7286..acce9aa 100644
--- a/modules/ext/mindmap.js
+++ b/modules/ext/mindmap.js
@@ -1075,6 +1075,61 @@ class MindmapModule {
}
}
}
+ _svgFullscreen(args) {
+ const cur = this._ensureMindmap();
+ if (!cur) return this.terminal.print('No mindmap. Create one with mindmap new first.', 'warning');
+ const name = args[0] || this.current || 'mindmap';
+ const layout = this._layoutTree(cur.root);
+ const colorMap = {
+ red: '#fb7185', green: '#34d399', yellow: '#fbbf24', blue: '#818cf8',
+ cyan: '#22d3ee', magenta: '#e879f9', white: '#e0e0e0'
+ };
+ const xShift = layout.minX === Infinity ? 0 : -layout.minX;
+ const svgWidth = layout.width;
+ const svgHeight = layout.height;
+ let svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + svgWidth + ' ' + svgHeight + '" style="background:#0a0a14;font-family:monospace;">';
+ for (const e of layout.edges) {
+ const sx = e.x1 + xShift + 30, sy = e.y1;
+ const ex = e.x2 + xShift + 30, ey = e.y2;
+ const mx = (sx + ex) / 2;
+ svg += '<path d="M' + sx + ',' + sy + ' C' + mx + ',' + (sy + 20) + ' ' + mx + ',' + (ey - 20) + ' ' + ex + ',' + ey + '" fill="none" stroke="#2a2a4a" stroke-width="1.5"/>';
+ }
+ for (const n of layout.nodes) {
+ const nx = n.x + xShift + 30;
+ const color = colorMap[n.color] || '#818cf8';
+ const fontSize = n.w < 120 ? '9' : '10';
+ const text = n.text.length > 18 ? n.text.substring(0, 16) + '..' : n.text;
+ svg += '<rect x="' + nx + '" y="' + n.y + '" width="' + n.w + '" height="' + n.h + '" rx="6" fill="' + color + '18" stroke="' + color + '" stroke-width="1.5"/>';
+ svg += '<text x="' + (nx + n.w / 2) + '" y="' + (n.y + n.h / 2 + 4) + '" text-anchor="middle" fill="#e0e0e0" font-size="' + fontSize + '" font-family="monospace">' + this._escapeHtml(text) + '</text>';
+ }
+ svg += '</svg>';
+
+ // Create fullscreen overlay
+ const overlay = document.createElement('div');
+ overlay.id = 'mindmap-svg-overlay';
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#0a0a14;display:flex;align-items:center;justify-content:center;overflow:auto';
+ overlay.innerHTML = svg;
+ document.body.appendChild(overlay);
+
+ // Close button
+ const closeBtn = document.createElement('button');
+ closeBtn.textContent = '✕';
+ closeBtn.style.cssText = 'position:fixed;top:12px;right:12px;z-index:10000;background:rgba(0,0,0,0.6);color:#e0e0e0;border:1px solid #444;border-radius:6px;padding:6px 14px;font-size:16px;cursor:pointer;font-family:monospace';
+ closeBtn.addEventListener('click', () => { overlay.remove(); closeBtn.remove(); });
+ document.body.appendChild(closeBtn);
+
+ // Escape key
+ const escHandler = (e) => {
+ if (e.key === 'Escape') {
+ overlay.remove();
+ closeBtn.remove();
+ document.removeEventListener('keydown', escHandler);
+ }
+ };
+ document.addEventListener('keydown', escHandler);
+
+ this.terminal.print(`🖼️ SVG fullscreen: ${layout.nodes.length} nodes, ${svgWidth}x${svgHeight}`, 'success');
+ }
// ── Main dispatcher ──
registerCommands() {
this.commands['mindmap'] = (args) => {
@@ -1116,7 +1171,15 @@ class MindmapModule {
case 'clone': return this._clone(rest);
case 'archive': return this._archive(rest);
case 'refresh-html': return this._refreshHtml(rest);
- case 'svg': return this._svg(rest);
+ case 'svg': {
+ const fsIdx = rest.indexOf('--fullscreen');
+ if (fsIdx >= 0) {
+ const clean = rest.filter(a => a !== '--fullscreen');
+ return this._svgFullscreen(clean);
+ }
+ return this._svg(rest);
+ }
+ case 'svg-fullscreen': return this._svgFullscreen(rest);
default: this.terminal.print('Unknown subcommand: ' + sub + '. Try: mindmap help', 'warning');
}
};
diff --git a/modules/ext/three.js b/modules/ext/three.js
new file mode 100644
index 0000000..d72afa1
--- /dev/null
+++ b/modules/ext/three.js
@@ -0,0 +1,1176 @@
+// newOS4 Three.js 3D Module (v4.5.82)
+// Commands: three <subcommand> [...]
+// Two modes: top panel (default) + fullscreen with terminal overlay
+// Lazy-loads Three.js from /downloads/ on first use.
+
+class ThreeModule {
+ constructor() {
+ this.commands = {};
+ this.commandsMeta = {
+ 'three': { argType: 'enum', enumFn: '_listSubcommands' },
+ };
+ this.terminal = null;
+ this._loaded = false;
+ this._scene = null;
+ this._camera = null;
+ this._renderer = null;
+ this._controls = null;
+ this._objects = [];
+ this._animId = null;
+ this._container = null;
+ this._closeBtn = null;
+ this._idCounter = 0;
+ this._orbitEnabled = true;
+ this._backgroundColor = 0x0a0a14;
+ this._THREE = null;
+ this._mode = 'top'; // 'top' or 'full'
+ this._visible = false;
+ this._savedTerminalStyles = null;
+ this._savedInputStyles = null;
+ this._savedHeaderStyles = null;
+ this._escHandler = null;
+ }
+
+ requirements() {
+ const hasWebGL = (() => {
+ try {
+ const c = document.createElement('canvas');
+ return !!(c.getContext('webgl') || c.getContext('experimental-webgl'));
+ } catch (e) { return false; }
+ })();
+ if (!hasWebGL) {
+ return { met: false, missing: ['WebGL not supported in this browser'] };
+ }
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'three': 'three <subcommand> [...] — 3D scene builder (Three.js). Subcommands: help, scene, fullscreen, hide, mode, add, camera, orbit, clear, list, remove, mindmap, export, preset, bgcolor. Run `three help` for full list.'
+ };
+ }
+
+ _listSubcommands() {
+ return ['help', 'scene', 'fullscreen', 'hide', 'mode', 'add', 'camera', 'orbit', 'clear', 'list', 'remove', 'mindmap', 'export', 'preset', 'bgcolor'];
+ }
+
+ registerCommands() {
+ this.commands['three'] = (args) => this._dispatch(args);
+ }
+
+ // ── Lazy load Three.js ──
+
+ async _ensureLoaded() {
+ if (this._loaded) return true;
+ if (typeof THREE !== 'undefined') {
+ this._THREE = THREE;
+ this._loaded = true;
+ return true;
+ }
+ this.terminal.print('📦 Loading Three.js (590KB)...', 'info');
+ try {
+ await this._loadScript('downloads/three.min.js');
+ await this._loadScript('downloads/OrbitControls.js');
+ this._THREE = THREE;
+ this._loaded = true;
+ this.terminal.print('✅ Three.js loaded', 'success');
+ return true;
+ } catch (e) {
+ this.terminal.print('❌ Failed to load Three.js: ' + e.message, 'error');
+ return false;
+ }
+ }
+
+ _loadScript(url) {
+ return new Promise((resolve, reject) => {
+ const s = document.createElement('script');
+ s.src = url;
+ s.onload = () => { s.remove(); resolve(); };
+ s.onerror = () => { s.remove(); reject(new Error('Failed to load ' + url)); };
+ document.head.appendChild(s);
+ });
+ }
+
+ // ── Container management ──
+
+ _createContainer() {
+ if (this._container) return;
+
+ // Main container
+ this._container = document.createElement('div');
+ this._container.id = 'three-canvas-container';
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw;
+ z-index: 400;
+ pointer-events: none;
+ display: none;
+ `;
+ document.body.appendChild(this._container);
+
+ // Close button
+ this._closeBtn = document.createElement('button');
+ this._closeBtn.id = 'three-close-btn';
+ this._closeBtn.textContent = '✕';
+ this._closeBtn.title = 'Close 3D view (Esc)';
+ this._closeBtn.style.cssText = `
+ position: fixed;
+ top: 12px; right: 12px;
+ z-index: 9999;
+ background: rgba(0,0,0,0.6);
+ color: #e0e0e0;
+ border: 1px solid #444;
+ border-radius: 6px;
+ padding: 6px 14px;
+ font-size: 16px;
+ cursor: pointer;
+ font-family: monospace;
+ display: none;
+ transition: background 0.15s;
+ `;
+ this._closeBtn.addEventListener('mouseenter', () => { this._closeBtn.style.background = 'rgba(200,50,50,0.7)'; });
+ this._closeBtn.addEventListener('mouseleave', () => { this._closeBtn.style.background = 'rgba(0,0,0,0.6)'; });
+ this._closeBtn.addEventListener('click', () => this._hideContainer());
+ document.body.appendChild(this._closeBtn);
+
+ // Escape key handler
+ this._escHandler = (e) => {
+ if (e.key === 'Escape' && this._visible) {
+ this._hideContainer();
+ }
+ };
+ document.addEventListener('keydown', this._escHandler);
+ }
+
+ _showContainer(mode) {
+ if (!this._container) return;
+ this._mode = mode || this._mode;
+ this._visible = true;
+
+ // Show close button
+ this._closeBtn.style.display = 'block';
+
+ if (this._mode === 'top') {
+ this._applyTopPanel();
+ } else {
+ this._applyFullscreen();
+ }
+
+ this._startLoop();
+ }
+
+ _applyTopPanel() {
+ // 3D container: top 50vh
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw; height: 50vh;
+ z-index: 400;
+ pointer-events: auto;
+ display: block;
+ `;
+
+ // Terminal: push down
+ const term = document.getElementById('terminal-container');
+ if (term) {
+ if (!this._savedTerminalStyles) {
+ this._savedTerminalStyles = { height: term.style.height, marginTop: term.style.marginTop };
+ }
+ term.style.marginTop = '50vh';
+ term.style.height = '50vh';
+ }
+
+ // Input area: stays at bottom
+ const inputArea = document.getElementById('input-area');
+ if (inputArea) {
+ if (!this._savedInputStyles) {
+ this._savedInputStyles = { position: inputArea.style.position, bottom: inputArea.style.bottom };
+ }
+ }
+
+ // Resize renderer
+ this._resizeRenderer(window.innerWidth, window.innerHeight / 2);
+ }
+
+ _applyFullscreen() {
+ // 3D container: full viewport
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw; height: 100vh;
+ z-index: 400;
+ pointer-events: auto;
+ display: block;
+ `;
+
+ // Terminal: float as overlay
+ const term = document.getElementById('terminal-container');
+ if (term) {
+ if (!this._savedTerminalStyles) {
+ this._savedTerminalStyles = { height: term.style.height, marginTop: term.style.marginTop };
+ }
+ term.style.cssText = `
+ position: fixed;
+ bottom: 60px;
+ left: 20px;
+ right: 20px;
+ max-height: 50vh;
+ z-index: 401;
+ background: rgba(0,0,0,0.7);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border-radius: 8px;
+ padding: 10px;
+ overflow-y: auto;
+ border: 1px solid #333;
+ `;
+ }
+
+ // Input area: fixed at bottom
+ const inputArea = document.getElementById('input-area');
+ if (inputArea) {
+ if (!this._savedInputStyles) {
+ this._savedInputStyles = { position: inputArea.style.position, bottom: inputArea.style.bottom };
+ }
+ inputArea.style.cssText = `
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 402;
+ background: rgba(0,0,0,0.85);
+ padding: 8px 12px;
+ border-top: 1px solid #333;
+ `;
+ }
+
+ // Header: hide
+ const header = document.querySelector('header');
+ if (header) {
+ if (!this._savedHeaderStyles) {
+ this._savedHeaderStyles = { display: header.style.display };
+ }
+ header.style.display = 'none';
+ }
+
+ this._resizeRenderer(window.innerWidth, window.innerHeight);
+ }
+
+ _restoreTerminal() {
+ const term = document.getElementById('terminal-container');
+ if (term && this._savedTerminalStyles) {
+ term.style.cssText = '';
+ term.style.height = this._savedTerminalStyles.height || '';
+ term.style.marginTop = this._savedTerminalStyles.marginTop || '';
+ term.style.position = '';
+ term.style.bottom = '';
+ term.style.left = '';
+ term.style.right = '';
+ term.style.maxHeight = '';
+ term.style.zIndex = '';
+ term.style.background = '';
+ term.style.backdropFilter = '';
+ term.style.borderRadius = '';
+ term.style.padding = '';
+ term.style.overflowY = '';
+ term.style.border = '';
+ }
+
+ const inputArea = document.getElementById('input-area');
+ if (inputArea && this._savedInputStyles) {
+ inputArea.style.cssText = '';
+ inputArea.style.position = this._savedInputStyles.position || '';
+ inputArea.style.bottom = this._savedInputStyles.bottom || '';
+ }
+
+ const header = document.querySelector('header');
+ if (header && this._savedHeaderStyles) {
+ header.style.display = this._savedHeaderStyles.display || '';
+ }
+ }
+
+ _hideContainer() {
+ if (!this._container) return;
+ this._visible = false;
+ this._container.style.display = 'none';
+ this._container.style.pointerEvents = 'none';
+ if (this._closeBtn) this._closeBtn.style.display = 'none';
+
+ // Pause animation (don't cancel — scene stays intact)
+ if (this._animId) {
+ cancelAnimationFrame(this._animId);
+ this._animId = null;
+ }
+
+ this._restoreTerminal();
+ }
+
+ _resizeRenderer(w, h) {
+ if (this._renderer && this._camera) {
+ this._camera.aspect = w / h;
+ this._camera.updateProjectionMatrix();
+ this._renderer.setSize(w, h);
+ }
+ }
+
+ // ── Scene management ──
+
+ _createScene() {
+ const T = this._THREE;
+ this._scene = new T.Scene();
+ this._scene.background = new T.Color(this._backgroundColor);
+
+ this._camera = new T.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
+ this._camera.position.set(0, 5, 15);
+
+ this._createContainer();
+ this._renderer = new T.WebGLRenderer({ antialias: true, alpha: false });
+ this._renderer.setSize(window.innerWidth, window.innerHeight);
+ this._renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ this._renderer.shadowMap.enabled = true;
+ this._container.appendChild(this._renderer.domElement);
+
+ // WebGL context loss handling
+ this._renderer.domElement.addEventListener('webglcontextlost', this._webglLostHandler = (e) => {
+ e.preventDefault();
+ this._stopLoop();
+ this.terminal?.print('⚠ WebGL context lost', 'warning');
+ });
+ this._renderer.domElement.addEventListener('webglcontextrestored', this._webglRestoredHandler = () => {
+ this._startLoop();
+ this.terminal?.print('✅ WebGL context restored', 'success');
+ });
+
+ this._controls = new T.OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.enableDamping = true;
+ this._controls.dampingFactor = 0.08;
+ this._controls.minDistance = 2;
+ this._controls.maxDistance = 500;
+
+ // Lights
+ const ambient = new T.AmbientLight(0x404060);
+ this._scene.add(ambient);
+ const dirLight = new T.DirectionalLight(0xffffff, 1);
+ dirLight.position.set(10, 20, 10);
+ dirLight.castShadow = true;
+ this._scene.add(dirLight);
+ const fillLight = new T.DirectionalLight(0x8888ff, 0.4);
+ fillLight.position.set(-10, 0, -10);
+ this._scene.add(fillLight);
+
+ // Grid
+ const grid = new T.GridHelper(30, 20, 0x00ff88, 0x333355);
+ this._scene.add(grid);
+
+ // Resize handler — remove old first to prevent leak
+ if (window._threeResizeHandler) {
+ window.removeEventListener('resize', window._threeResizeHandler);
+ }
+ window._threeResizeHandler = () => {
+ if (this._camera && this._renderer && this._visible) {
+ if (this._mode === 'top') {
+ this._resizeRenderer(window.innerWidth, window.innerHeight / 2);
+ } else {
+ this._resizeRenderer(window.innerWidth, window.innerHeight);
+ }
+ }
+ };
+ window.addEventListener('resize', window._threeResizeHandler);
+
+ this._objects = [];
+ this._idCounter = 0;
+ }
+
+ _startLoop() {
+ if (this._animId) cancelAnimationFrame(this._animId);
+ const loop = () => {
+ this._animId = requestAnimationFrame(loop);
+ if (this._controls) this._controls.update();
+ if (this._renderer && this._scene && this._camera) {
+ this._renderer.render(this._scene, this._camera);
+ }
+ };
+ loop();
+ }
+
+ _stopLoop() {
+ if (this._animId) {
+ cancelAnimationFrame(this._animId);
+ this._animId = null;
+ }
+ }
+
+ // ── Lifecycle ──
+
+ destroy() {
+ this._stopLoop();
+ this._hideContainer();
+ // Remove event listeners
+ if (window._threeResizeHandler) {
+ window.removeEventListener('resize', window._threeResizeHandler);
+ window._threeResizeHandler = null;
+ }
+ if (this._escHandler) {
+ document.removeEventListener('keydown', this._escHandler);
+ this._escHandler = null;
+ }
+ if (this._renderer) {
+ if (this._webglLostHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextlost', this._webglLostHandler);
+ }
+ if (this._webglRestoredHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextrestored', this._webglRestoredHandler);
+ }
+ this._renderer.dispose();
+ this._renderer = null;
+ }
+ // Dispose all GPU resources
+ this._disposeAllObjects();
+ // Remove DOM elements
+ if (this._container) {
+ this._container.remove();
+ this._container = null;
+ }
+ if (this._closeBtn) {
+ this._closeBtn.remove();
+ this._closeBtn = null;
+ }
+ this._scene = null;
+ this._camera = null;
+ this._controls = null;
+ this._loaded = false;
+ this._visible = false;
+ }
+
+ _disposeAllObjects() {
+ for (const obj of this._objects) {
+ if (this._scene) this._scene.remove(obj.mesh);
+ if (obj.mesh.geometry) obj.mesh.geometry.dispose();
+ if (obj.mesh.material) {
+ const mats = Array.isArray(obj.mesh.material) ? obj.mesh.material : [obj.mesh.material];
+ mats.forEach(m => {
+ if (m.map) m.map.dispose();
+ m.dispose();
+ });
+ }
+ }
+ this._objects = [];
+ this._idCounter = 0;
+ }
+
+ _genId() {
+ return 'obj_' + (++this._idCounter);
+ }
+
+ // ── Object creation ──
+
+ _addBox(args) {
+ const T = this._THREE;
+ const size = parseFloat(args[0]) || 1;
+ const color = args[1] || '#818cf8';
+ const geo = new T.BoxGeometry(size, size, size);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.6 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Box ' + id });
+ this.terminal.print(`Added box (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addSphere(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 0.8;
+ const color = args[1] || '#34d399';
+ const geo = new T.SphereGeometry(radius, 32, 32);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.2, roughness: 0.4 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Sphere ' + id });
+ this.terminal.print(`Added sphere (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addTorus(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 1;
+ const tube = parseFloat(args[1]) || 0.3;
+ const color = args[2] || '#e879f9';
+ const geo = new T.TorusGeometry(radius, tube, 16, 32);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.4, roughness: 0.3 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Torus ' + id });
+ this.terminal.print(`Added torus (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addText(args) {
+ if (!args[0]) return this.terminal.print('Usage: three add text <text> [color]', 'warning');
+ const T = this._THREE;
+ const text = args[0];
+ const color = args[1] || '#22d3ee';
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 64;
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, 256, 64);
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = color;
+ ctx.fillText(text, 128, 32);
+ const texture = new T.CanvasTexture(canvas);
+ const mat = new T.SpriteMaterial({ map: texture, transparent: true });
+ const sprite = new T.Sprite(mat);
+ sprite.scale.set(4, 1, 1);
+ const id = this._genId();
+ this._scene.add(sprite);
+ this._objects.push({ mesh: sprite, id, label: 'Text: ' + text });
+ this.terminal.print(`Added text "${text}" (${id})`, 'success');
+ return id;
+ }
+
+ _addParticles(args) {
+ const T = this._THREE;
+ const count = parseInt(args[0]) || 500;
+ const color = args[1] || '#00ff88';
+ const geo = new T.BufferGeometry();
+ const positions = new Float32Array(count * 3);
+ for (let i = 0; i < count * 3; i++) {
+ positions[i] = (Math.random() - 0.5) * 40;
+ }
+ geo.setAttribute('position', new T.BufferAttribute(positions, 3));
+ const mat = new T.PointsMaterial({
+ color, size: 0.15, transparent: true, opacity: 0.8, blending: T.AdditiveBlending
+ });
+ const points = new T.Points(geo, mat);
+ const id = this._genId();
+ this._scene.add(points);
+ this._objects.push({ mesh: points, id, label: 'Particles (' + count + ')' });
+ this.terminal.print(`Added ${count} particles (${id})`, 'success');
+ return id;
+ }
+
+ _addLine(args) {
+ const T = this._THREE;
+ if (args.length < 6) return this.terminal.print('Usage: three add line <x1> <y1> <z1> <x2> <y2> <z2> [color]', 'warning');
+ const x1 = parseFloat(args[0]), y1 = parseFloat(args[1]), z1 = parseFloat(args[2]);
+ const x2 = parseFloat(args[3]), y2 = parseFloat(args[4]), z2 = parseFloat(args[5]);
+ const color = args[6] || '#00ff88';
+ const points = [
+ new T.Vector3(x1, y1, z1),
+ new T.Vector3(x2, y2, z2)
+ ];
+ const geo = new T.BufferGeometry().setFromPoints(points);
+ const mat = new T.LineBasicMaterial({ color });
+ const line = new T.Line(geo, mat);
+ const id = this._genId();
+ this._scene.add(line);
+ this._objects.push({ mesh: line, id, label: 'Line ' + id });
+ this.terminal.print(`Added line (${id})`, 'success');
+ return id;
+ }
+
+ // ── Position helpers ──
+
+ _setPosition(id, x, y, z) {
+ const obj = this._objects.find(o => o.id === id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.mesh.position.set(x, y, z);
+ this.terminal.print(`Moved ${id} to (${x}, ${y}, ${z})`, 'success');
+ }
+
+ _setRotation(id, rx, ry, rz) {
+ const obj = this._objects.find(o => o.id === id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.mesh.rotation.set(rx, ry, rz);
+ this.terminal.print(`Rotated ${id}`, 'success');
+ }
+
+ // ── 3D Mindmap ──
+
+ _renderMindmap(args) {
+ const mm = this.terminal.moduleManager.modules['MindmapModule'];
+ if (!mm) return this.terminal.print('MindmapModule not loaded', 'error');
+ const cur = mm._ensureMindmap();
+ if (!cur) return this.terminal.print('No mindmap. Create one with mindmap new first.', 'warning');
+ const root = cur.root;
+
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+
+ const T = this._THREE;
+ const colorMap = {
+ red: '#fb7185', green: '#34d399', yellow: '#fbbf24', blue: '#818cf8',
+ cyan: '#22d3ee', magenta: '#e879f9', white: '#e0e0e0'
+ };
+
+ const nodes = [];
+ const edges = [];
+
+ const layout = (node, parentPos, depth, angle, spread) => {
+ const pos = { x: 0, y: 0, z: 0 };
+ if (!parentPos) {
+ pos.x = 0; pos.y = 4; pos.z = 0;
+ } else {
+ const radius = 2 + depth * 2.5;
+ pos.x = parentPos.x + Math.cos(angle) * radius;
+ pos.z = parentPos.z + Math.sin(angle) * radius;
+ pos.y = parentPos.y - 1.5;
+ }
+ nodes.push({ ...node, pos, depth, color: colorMap[node.color] || '#818cf8' });
+ if (parentPos) edges.push({ from: parentPos, to: pos });
+ if (node.children) {
+ const count = node.children.length;
+ const startAngle = angle - spread / 2;
+ node.children.forEach((child, i) => {
+ const childAngle = startAngle + (i / (count || 1)) * spread;
+ layout(child, pos, depth + 1, childAngle, Math.min(spread * 0.7, Math.PI));
+ });
+ }
+ };
+ layout(root, null, 0, 0, Math.PI * 1.5);
+
+ const nodeMeshes = [];
+ for (const n of nodes) {
+ const radius = n.depth === 0 ? 0.6 : 0.3 + (1 / (n.depth + 1)) * 0.4;
+ const geo = new T.SphereGeometry(radius, 16, 16);
+ const mat = new T.MeshStandardMaterial({
+ color: n.color, emissive: n.color, emissiveIntensity: 0.15,
+ metalness: 0.3, roughness: 0.5
+ });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.set(n.pos.x, n.pos.y, n.pos.z);
+ mesh.castShadow = true;
+ mesh.userData = { id: n.id, text: n.text, depth: n.depth };
+ this._scene.add(mesh);
+ nodeMeshes.push(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: 'Mindmap: ' + n.text });
+
+ // Text label sprite
+ const canvas = document.createElement('canvas');
+ canvas.width = 512; canvas.height = 96;
+ const ctx = canvas.getContext('2d');
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillStyle = n.color;
+ const label = n.text.length > 16 ? n.text.substring(0, 14) + '..' : n.text;
+ ctx.fillText(label, 256, 48);
+ const tex = new T.CanvasTexture(canvas);
+ const spriteMat = new T.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
+ const sprite = new T.Sprite(spriteMat);
+ sprite.position.set(n.pos.x, n.pos.y + radius + 0.5, n.pos.z);
+ sprite.scale.set(2.5, 0.5, 1);
+ this._scene.add(sprite);
+ this._objects.push({ mesh: sprite, id: this._genId(), label: 'Label: ' + n.text });
+ }
+
+ for (const e of edges) {
+ const pts = [
+ new T.Vector3(e.from.x, e.from.y, e.from.z),
+ new T.Vector3((e.from.x + e.to.x) / 2, (e.from.y + e.to.y) / 2 - 0.5, (e.from.z + e.to.z) / 2),
+ new T.Vector3(e.to.x, e.to.y, e.to.z)
+ ];
+ const curve = new T.QuadraticBezierCurve3(pts[0], pts[1], pts[2]);
+ const curvePoints = curve.getPoints(20);
+ const geo = new T.BufferGeometry().setFromPoints(curvePoints);
+ const mat = new T.LineBasicMaterial({ color: 0x444466, transparent: true, opacity: 0.5 });
+ const line = new T.Line(geo, mat);
+ this._scene.add(line);
+ this._objects.push({ mesh: line, id: this._genId(), label: 'Edge' });
+ }
+
+ if (nodeMeshes.length > 0) {
+ const box = new T.Box3().setFromObjects(nodeMeshes);
+ const center = box.getCenter(new T.Vector3());
+ const size = box.getSize(new T.Vector3());
+ const maxDim = Math.max(size.x, size.y, size.z);
+ const dist = maxDim * 1.5 + 5;
+ this._camera.position.set(center.x + dist * 0.5, center.y + dist * 0.3, center.z + dist);
+ this._controls.target.copy(center);
+ this._controls.update();
+ }
+
+ this.terminal.print(`🧊 3D mindmap rendered: ${nodes.length} nodes, ${edges.length} edges`, 'success');
+ }
+
+ // ── Presets ──
+
+ _presetSolarSystem() {
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+ const T = this._THREE;
+
+ const sunGeo = new T.SphereGeometry(1.5, 32, 32);
+ const sunMat = new T.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xff6600, emissiveIntensity: 0.5 });
+ const sun = new T.Mesh(sunGeo, sunMat);
+ this._scene.add(sun);
+ this._objects.push({ mesh: sun, id: this._genId(), label: 'Sun' });
+
+ const planets = [
+ { name: 'Mercury', radius: 0.15, orbit: 2.5, color: 0xaaaaaa, speed: 0.02 },
+ { name: 'Venus', radius: 0.25, orbit: 4, color: 0xffcc88, speed: 0.015 },
+ { name: 'Earth', radius: 0.3, orbit: 5.5, color: 0x4488ff, speed: 0.01 },
+ { name: 'Mars', radius: 0.2, orbit: 7, color: 0xff6644, speed: 0.008 },
+ { name: 'Jupiter', radius: 0.6, orbit: 9, color: 0xddbb88, speed: 0.005 },
+ { name: 'Saturn', radius: 0.5, orbit: 11, color: 0xeeddbb, speed: 0.003 },
+ ];
+ for (const p of planets) {
+ const geo = new T.SphereGeometry(p.radius, 16, 16);
+ const mat = new T.MeshStandardMaterial({ color: p.color });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.x = p.orbit;
+ mesh.userData = { orbit: p.orbit, speed: p.speed, angle: Math.random() * Math.PI * 2 };
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: p.name, _animate: true, _data: mesh.userData });
+
+ const ringPoints = [];
+ for (let i = 0; i <= 64; i++) {
+ const a = (i / 64) * Math.PI * 2;
+ ringPoints.push(new T.Vector3(Math.cos(a) * p.orbit, 0, Math.sin(a) * p.orbit));
+ }
+ const ringGeo = new T.BufferGeometry().setFromPoints(ringPoints);
+ const ringMat = new T.LineBasicMaterial({ color: 0x333355, transparent: true, opacity: 0.3 });
+ const ring = new T.Line(ringGeo, ringMat);
+ this._scene.add(ring);
+ this._objects.push({ mesh: ring, id: this._genId(), label: 'Orbit: ' + p.name });
+ }
+ this.terminal.print('🌌 Solar system preset loaded', 'success');
+ }
+
+ _presetGrid(args) {
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+ const T = this._THREE;
+ const size = parseInt(args[0]) || 5;
+ const spacing = parseFloat(args[1]) || 2;
+
+ for (let x = -size; x <= size; x++) {
+ for (let z = -size; z <= size; z++) {
+ const geo = new T.BoxGeometry(0.3, 0.3, 0.3);
+ const hue = ((x + size) / (size * 2)) * 0.6 + 0.5;
+ const color = new T.Color().setHSL(hue, 0.8, 0.5);
+ const mat = new T.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.1 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.set(x * spacing, 0, z * spacing);
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: `Grid (${x},${z})` });
+ }
+ }
+ this.terminal.print(`📐 Grid preset: ${(size*2+1)**2} cubes`, 'success');
+ }
+
+ // ── Export ──
+
+ _export(args) {
+ if (!this._scene) return this.terminal.print('No scene to export', 'warning');
+ const rawName = args[0] || 'three-scene';
+ const name = rawName.replace(/[<>&"']/g, function(c) { return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]; });
+ const T = this._THREE;
+
+ const objects = [];
+ for (const obj of this._objects) {
+ const m = obj.mesh;
+ if (m.type === 'Mesh' || m.type === 'Points' || m.type === 'Sprite' || m.type === 'Line') {
+ const hexColor = m.material && m.material.color ? m.material.color.getHex() : null;
+ objects.push({
+ type: m.type, id: obj.id, label: obj.label,
+ position: m.position.toArray(), rotation: m.rotation.toArray(), scale: m.scale.toArray(),
+ color: hexColor
+ });
+ }
+ }
+
+ const cameraPos = this._camera ? this._camera.position.toArray() : [0, 5, 15];
+ const bgColor = '#' + new T.Color(this._backgroundColor).getHexString();
+
+ const html = `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>3D Scene: ${name}</title>
+ <script src="three.min.js"></script>
+ <script src="OrbitControls.js"></script>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { background: ${bgColor}; overflow: hidden; font-family: monospace; }
+ #info {
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
+ color: #888; font-size: 12px; background: rgba(0,0,0,0.6);
+ padding: 8px 16px; border-radius: 8px; pointer-events: none;
+ border: 1px solid #333;
+ }
+ </style>
+</head>
+<body>
+ <div id="info">${name} — ${objects.length} objects — Drag to orbit, scroll to zoom</div>
+ <script>
+ const scene = new THREE.Scene();
+ scene.background = new THREE.Color('${bgColor}');
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
+ camera.position.set(${cameraPos.join(', ')});
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ document.body.appendChild(renderer.domElement);
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ scene.add(new THREE.AmbientLight(0x404060));
+ const dl = new THREE.DirectionalLight(0xffffff, 1);
+ dl.position.set(10, 20, 10);
+ scene.add(dl);
+ const fl = new THREE.DirectionalLight(0x8888ff, 0.4);
+ fl.position.set(-10, 0, -10);
+ scene.add(fl);
+ scene.add(new THREE.GridHelper(30, 20, 0x00ff88, 0x333355));
+ const data = ${JSON.stringify(objects)};
+ data.forEach(function(d) {
+ var mesh;
+ if (d.type === 'Mesh') {
+ var geo = new THREE.SphereGeometry(0.5, 16, 16);
+ var mat = new THREE.MeshStandardMaterial({ color: d.color != null ? d.color : 0x818cf8 });
+ mesh = new THREE.Mesh(geo, mat);
+ } else if (d.type === 'Points') {
+ var geo = new THREE.BufferGeometry();
+ var pos = new Float32Array(500 * 3);
+ for (var i = 0; i < 1500; i++) pos[i] = (Math.random() - 0.5) * 40;
+ geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
+ var mat = new THREE.PointsMaterial({ color: d.color != null ? d.color : 0x00ff88, size: 0.15, transparent: true });
+ mesh = new THREE.Points(geo, mat);
+ } else if (d.type === 'Sprite') {
+ var canvas = document.createElement('canvas');
+ canvas.width = 512; canvas.height = 96;
+ var ctx = canvas.getContext('2d');
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillStyle = d.color != null ? '#' + d.color.toString(16).padStart(6, '0') : '#22d3ee';
+ ctx.fillText(d.label || '?', 256, 48);
+ var tex = new THREE.CanvasTexture(canvas);
+ var mat = new THREE.SpriteMaterial({ map: tex, transparent: true });
+ mesh = new THREE.Sprite(mat);
+ } else { return; }
+ mesh.position.set(d.position[0], d.position[1], d.position[2]);
+ if (d.rotation) mesh.rotation.set(d.rotation[0], d.rotation[1], d.rotation[2]);
+ if (d.scale) mesh.scale.set(d.scale[0], d.scale[1], d.scale[2]);
+ scene.add(mesh);
+ });
+ function animate() {
+ requestAnimationFrame(animate);
+ controls.update();
+ renderer.render(scene, camera);
+ }
+ animate();
+ window.addEventListener('resize', function() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ });
+ </script>
+</body>
+</html>`;
+
+ try {
+ const fs = this.terminal.fs;
+ fs.mkdirRecursive('/exports');
+ fs.writeFile('/exports/' + name + '.html', html);
+ this.terminal.print(`💾 Exported to /exports/${name}.html (${(html.length / 1024).toFixed(1)}KB)`, 'success');
+ } catch (e) {
+ this.terminal.print('Failed to save export: ' + e.message, 'error');
+ }
+ }
+
+ // ── Scene helpers ──
+
+ _clearScene() {
+ if (!this._scene) return;
+ this._disposeAllObjects();
+ }
+
+ _listObjects() {
+ if (this._objects.length === 0) return this.terminal.print('No objects in scene', 'info');
+ this.terminal.print(`=== 3D Objects (${this._objects.length}) ===`, 'header');
+ for (const obj of this._objects) {
+ const pos = obj.mesh.position;
+ this.terminal.print(` ${obj.id} ${obj.label} @ (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`);
+ }
+ }
+
+ _removeObject(id) {
+ const idx = this._objects.findIndex(o => o.id === id);
+ if (idx === -1) return this.terminal.print('Object not found: ' + id, 'error');
+ const obj = this._objects[idx];
+ this._scene.remove(obj.mesh);
+ if (obj.mesh.geometry) obj.mesh.geometry.dispose();
+ if (obj.mesh.material) {
+ const mats = Array.isArray(obj.mesh.material) ? obj.mesh.material : [obj.mesh.material];
+ mats.forEach(m => {
+ if (m.map) m.map.dispose();
+ m.dispose();
+ });
+ }
+ this._objects.splice(idx, 1);
+ this.terminal.print(`Removed ${id}`, 'success');
+ }
+
+ _setBgColor(args) {
+ if (!args[0]) return this.terminal.print('Usage: three bgcolor <hex> (e.g. #0a0a14)', 'warning');
+ const color = parseInt(args[0].replace('#', ''), 16);
+ if (isNaN(color)) return this.terminal.print('Invalid color', 'warning');
+ this._backgroundColor = color;
+ if (this._scene) this._scene.background = new this._THREE.Color(color);
+ this.terminal.print(`Background set to ${args[0]}`, 'success');
+ }
+
+ // ── Main dispatch ──
+
+ async _dispatch(args) {
+ if (!args[0] || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
+ this._printHelp();
+ return;
+ }
+
+ const loaded = await this._ensureLoaded();
+ if (!loaded && args[0] !== 'help') return;
+
+ const sub = args[0];
+ const rest = args.slice(1);
+
+ switch (sub) {
+ case 'scene':
+ if (!this._scene) this._createScene();
+ this._mode = 'top';
+ this._showContainer('top');
+ this.terminal.print('🎨 3D scene — top panel mode. Use three fullscreen for full view, Esc to close.', 'success');
+ break;
+ case 'fullscreen':
+ if (!this._scene) this._createScene();
+ this._mode = 'full';
+ this._showContainer('full');
+ this.terminal.print('🎨 3D scene — fullscreen mode. Terminal floats on top. Esc to close.', 'success');
+ break;
+ case 'mode': {
+ const mode = rest[0];
+ if (!mode || (mode !== 'top' && mode !== 'full')) {
+ return this.terminal.print('Usage: three mode top|full (current: ' + this._mode + ')', 'warning');
+ }
+ if (!this._scene) return this.terminal.print('No scene. Create one with three scene first.', 'warning');
+ this._mode = mode;
+ this._showContainer(mode);
+ this.terminal.print(`Switched to ${mode} panel mode`, 'success');
+ break;
+ }
+ case 'add': {
+ const type = rest[0];
+ const params = rest.slice(1);
+ if (!type) return this.terminal.print('Usage: three add box|sphere|torus|text|particles|line [params...]', 'warning');
+ if (!this._scene) this._createScene();
+ if (!this._visible) this._showContainer(this._mode);
+ switch (type) {
+ case 'box': this._addBox(params); break;
+ case 'sphere': this._addSphere(params); break;
+ case 'torus': this._addTorus(params); break;
+ case 'text': this._addText(params); break;
+ case 'particles': this._addParticles(params); break;
+ case 'line': this._addLine(params); break;
+ default: this.terminal.print('Unknown type: ' + type + '. Try: box, sphere, torus, text, particles, line', 'warning');
+ }
+ break;
+ }
+ case 'camera': {
+ if (rest.length < 3) return this.terminal.print('Usage: three camera <x> <y> <z>', 'warning');
+ if (!this._camera) return this.terminal.print('No scene. Create one with three scene first.', 'warning');
+ this._camera.position.set(parseFloat(rest[0]), parseFloat(rest[1]), parseFloat(rest[2]));
+ this._controls.update();
+ this.terminal.print(`Camera moved to (${rest[0]}, ${rest[1]}, ${rest[2]})`, 'success');
+ break;
+ }
+ case 'orbit': {
+ if (!this._controls) return this.terminal.print('No scene.', 'warning');
+ this._orbitEnabled = !this._orbitEnabled;
+ this._controls.enabled = this._orbitEnabled;
+ this.terminal.print(`Orbit controls: ${this._orbitEnabled ? 'ON' : 'OFF'}`, 'info');
+ break;
+ }
+ case 'clear':
+ this._clearScene();
+ this.terminal.print('Scene cleared', 'success');
+ break;
+ case 'list':
+ this._listObjects();
+ break;
+ case 'remove':
+ if (!rest[0]) return this.terminal.print('Usage: three remove <id>', 'warning');
+ this._removeObject(rest[0]);
+ break;
+ case 'pos':
+ if (rest.length < 4) return this.terminal.print('Usage: three pos <id> <x> <y> <z>', 'warning');
+ this._setPosition(rest[0], parseFloat(rest[1]), parseFloat(rest[2]), parseFloat(rest[3]));
+ break;
+ case 'mindmap':
+ this._renderMindmap(rest);
+ break;
+ case 'export':
+ this._export(rest);
+ break;
+ case 'preset': {
+ const preset = rest[0] || 'solar';
+ if (!this._scene) this._createScene();
+ if (!this._visible) this._showContainer(this._mode);
+ if (preset === 'solar') this._presetSolarSystem();
+ else if (preset === 'grid') this._presetGrid(rest.slice(1));
+ else this.terminal.print('Presets: solar, grid', 'warning');
+ break;
+ }
+ case 'bgcolor':
+ this._setBgColor(rest);
+ break;
+ case 'hide':
+ this._hideContainer();
+ this.terminal.print('3D view closed. Use three scene or three fullscreen to reopen.', 'info');
+ break;
+ default:
+ this.terminal.print('Unknown subcommand: ' + sub + '. Try: three help', 'warning');
+ }
+ }
+
+ _printHelp() {
+ this.terminal.print('=== Three.js 3D Module (v4.5.82) ===', 'header');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Scene ──', 'info');
+ this.terminal.print(' three scene Open 3D in top panel (default)');
+ this.terminal.print(' three fullscreen Open 3D fullscreen with terminal overlay');
+ this.terminal.print(' three mode top|full Switch mode without closing');
+ this.terminal.print(' three hide Close 3D view (or press Esc)');
+ this.terminal.print(' three clear Remove all objects');
+ this.terminal.print(' three bgcolor <hex> Set background color (e.g. #0a0a14)');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Objects ──', 'info');
+ this.terminal.print(' three add box [size] [color] Add a box');
+ this.terminal.print(' three add sphere [r] [color] Add a sphere');
+ this.terminal.print(' three add torus [r] [tube] [c] Add a torus');
+ this.terminal.print(' three add text <text> [color] Add 2D text sprite');
+ this.terminal.print(' three add particles [n] [c] Add particle system');
+ this.terminal.print(' three add line x1 y1 z1 x2 y2 z2 [color] Add a line');
+ this.terminal.print(' three list List all objects');
+ this.terminal.print(' three remove <id> Remove an object');
+ this.terminal.print(' three pos <id> <x> <y> <z> Move an object');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Camera & Controls ──', 'info');
+ this.terminal.print(' three camera <x> <y> <z> Set camera position');
+ this.terminal.print(' three orbit Toggle orbit controls');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Mindmap ──', 'info');
+ this.terminal.print(' three mindmap [name] Render current mindmap in 3D');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Presets ──', 'info');
+ this.terminal.print(' three preset solar Solar system');
+ this.terminal.print(' three preset grid [n] [spc] Color grid of cubes');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Export ──', 'info');
+ this.terminal.print(' three export [name] Export scene as standalone HTML');
+ this.terminal.print('', '');
+ this.terminal.print(' ── AI Tools ──', 'info');
+ this.terminal.print(' The AI can create 3D scenes via create_3d_scene tool.');
+ this.terminal.print(' The AI can render mindmaps in 3D via visualize_mindmap_3d tool.');
+ }
+
+ // ── AI Tool Interface ──
+
+ async createSceneFromDescription(description) {
+ const loaded = await this._ensureLoaded();
+ if (!loaded) return 'Failed to load Three.js';
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+
+ const T = this._THREE;
+ const desc = description.toLowerCase();
+ const results = [];
+
+ const boxMatch = desc.match(/(\d+)?\s*box(?:es)?/);
+ if (boxMatch) {
+ const count = parseInt(boxMatch[1]) || 1;
+ for (let i = 0; i < count; i++) {
+ const id = this._addBox(['0.8', '#818cf8']);
+ const angle = (i / count) * Math.PI * 2;
+ this._setPosition(id, Math.cos(angle) * 3, 0, Math.sin(angle) * 3);
+ results.push('box ' + id);
+ }
+ }
+
+ const sphereMatch = desc.match(/(\d+)?\s*sphere(?:s)?/);
+ if (sphereMatch) {
+ const count = parseInt(sphereMatch[1]) || 1;
+ for (let i = 0; i < count; i++) {
+ const id = this._addSphere(['0.6', '#34d399']);
+ const angle = (i / count) * Math.PI * 2 + 0.5;
+ this._setPosition(id, Math.cos(angle) * 3, 1, Math.sin(angle) * 3);
+ results.push('sphere ' + id);
+ }
+ }
+
+ if (desc.includes('particles') || desc.includes('stars') || desc.includes('starfield')) {
+ this._addParticles(['800', '#00ff88']);
+ results.push('particles');
+ }
+
+ const textMatch = desc.match(/text["']?[:]?\s*["']?([^"']+)["']?/);
+ if (textMatch) {
+ this._addText([textMatch[1].trim(), '#22d3ee']);
+ results.push('text');
+ }
+
+ if (desc.includes('torus') || desc.includes('donut') || desc.includes('ring')) {
+ this._addTorus(['1.2', '0.3', '#e879f9']);
+ results.push('torus');
+ }
+
+ if (desc.includes('solar') || desc.includes('planet')) {
+ this._presetSolarSystem();
+ return 'Created solar system preset';
+ }
+
+ if (desc.includes('grid')) {
+ this._presetGrid(['4', '2']);
+ return 'Created grid preset';
+ }
+
+ if (results.length === 0) {
+ this._addBox(['1', '#818cf8']);
+ this._addSphere(['0.6', '#34d399']);
+ this._setPosition('obj_1', -2, 0, 0);
+ this._setPosition('obj_2', 2, 0, 0);
+ results.push('default scene');
+ }
+
+ return 'Created 3D scene with: ' + results.join(', ');
+ }
+
+ async visualizeMindmap3D(name) {
+ const loaded = await this._ensureLoaded();
+ if (!loaded) return 'Failed to load Three.js';
+ const mm = this.terminal.moduleManager.modules['MindmapModule'];
+ if (!mm) return 'MindmapModule not loaded';
+ const cur = mm._ensureMindmap();
+ if (!cur) return 'No mindmap active. Create one first.';
+ this._renderMindmap(name ? [name] : []);
+ const count = this._objects.filter(o => o.label.startsWith('Mindmap:')).length;
+ return `Rendered mindmap "${cur.root.text}" in 3D: ${count} nodes`;
+ }
+}
+
+// Register
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new ThreeModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/three.js');
+}