// newOS4 Three.js 3D Module (v4.5.82) // Commands: three [...] // 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 [...] — 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 [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 [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 {'<':'<','>':'>','&':'&','"':'"',"'":'''}[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 = ` 3D Scene: ${name}
${name} — ${objects.length} objects — Drag to orbit, scroll to zoom
`; 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 (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 ', '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 ', 'warning'); this._removeObject(rest[0]); break; case 'pos': if (rest.length < 4) return this.terminal.print('Usage: three pos ', '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 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 [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 Remove an object'); this.terminal.print(' three pos Move an object'); this.terminal.print('', ''); this.terminal.print(' ── Camera & Controls ──', 'info'); this.terminal.print(' three camera 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'); }