// newOS4 Three.js 3D Module (v4.5.86) // Commands: three [...] // Two modes: top panel (default) + fullscreen with terminal overlay // Lazy-loads Three.js from /downloads/ on first use. // // Features: // - 3D Design: box, sphere, cylinder, cone, torus, torusKnot, text, particles, line // - Transforms: position, rotation, scale, color, material, duplicate, group // - Animations: rotate, bounce, float, orbit, keyframe, timeline // - Avatars: load, pose, look-at, speak, emotion // - Interaction: click, hover, follow, path // - Import/Export: GLTF, GLB, HTML // - Presets: solar system, grid, particle field // - Mindmap 3D rendering class ThreeModule { constructor() { this.commands = {}; this.commandsMeta = { 'three': { argType: 'enum', enumFn: '_listSubcommands' }, }; this.terminal = null; this._loaded = false; this._loadingPromise = null; this._scene = null; this._camera = null; this._renderer = null; this._controls = null; this._objects = new Map(); 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; this._animations = {}; // id -> { type, speed, active } this._timeline = []; // [{ time, id, props }] this._timelinePlaying = false; this._timelineTime = 0; this._clickHandlers = {}; // id -> command string this._hoverHandlers = {}; // id -> { enterColor, leaveColor } this._followTargets = {}; // id -> targetId this._paths = {}; // id -> [{x,y,z}, ...] this._pathProgress = {}; // id -> index this._avatar = null; // current avatar object this._avatarMood = 'neutral'; this._speechBubble = null; this._speechTimeout = null; this._raycaster = null; this._mouse = null; this._clickableObjects = []; this._clock = null; this._animTime = 0; this._importCache = {}; // URL -> loaded data this._bloomEnabled = true; this._composer = null; this._transformControls = null; this._selectedId = null; this._undoStack = []; this._redoStack = []; } 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). Run `three help` for full list.' }; } _listSubcommands() { return ['help', 'scene', 'fullscreen', 'hide', 'mode', 'add', 'camera', 'orbit', 'pos', 'rotate', 'scale', 'color', 'material', 'duplicate', 'group', 'ungroup', 'animate', 'stop', 'timeline', 'click', 'hover', 'follow', 'unfollow', 'path', 'avatar', 'speak', 'emotion', 'clear', 'list', 'remove', 'mindmap', 'export', 'import', 'preset', 'bgcolor', 'bloom', 'select', 'gizmo', 'undo', 'redo', 'rename', 'batch', 'snapshot']; } registerCommands() { this.commands['three'] = (args) => this._dispatch(args); } // ── Lazy load Three.js ── async _ensureLoaded() { if (this._loaded) return true; if (this._loadingPromise) return this._loadingPromise; if (typeof THREE !== 'undefined') { this._THREE = THREE; this._loaded = true; return true; } this._loadingPromise = this._doLoad(); return this._loadingPromise; } async _doLoad() { 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._loadingPromise = null; this.terminal.print('✅ Three.js loaded', 'success'); return true; } catch (e) { this._loadingPromise = null; 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; const timeout = setTimeout(() => { s.remove(); reject(new Error('Timeout loading ' + url)); }, 30000); s.onload = () => { clearTimeout(timeout); s.remove(); resolve(); }; s.onerror = () => { clearTimeout(timeout); s.remove(); reject(new Error('Failed to load ' + url)); }; document.head.appendChild(s); }); } // ── Container management ── _createContainer() { if (this._container) return; 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); 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); 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; this._closeBtn.style.display = 'block'; if (this._mode === 'top') { this._applyTopPanel(); } else { this._applyFullscreen(); } this._startLoop(); } _applyTopPanel() { this._container.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 50vh; z-index: 400; pointer-events: auto; display: block; `; 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'; } const inputArea = document.getElementById('input-area'); if (inputArea) { if (!this._savedInputStyles) { this._savedInputStyles = { position: inputArea.style.position, bottom: inputArea.style.bottom }; } } this._resizeRenderer(window.innerWidth, window.innerHeight / 2); } _applyFullscreen() { this._container.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 400; pointer-events: auto; display: block; `; 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; `; } 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; `; } 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'; 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() { // Dispose existing GPU resources before creating new ones if (this._renderer) { this._stopLoop(); if (this._controls) { this._controls.dispose(); this._controls = null; } 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; if (this._container) this._container.innerHTML = ''; } this._disposeAllObjects(); 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; // TransformControls gizmo this._initTransformControls(); // 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 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); } if (this._composer) { this._composer.setSize( this._mode === 'top' ? window.innerWidth : window.innerWidth, this._mode === 'top' ? window.innerHeight / 2 : window.innerHeight ); } } }; window.addEventListener('resize', window._threeResizeHandler); // Raycaster for click/hover this._raycaster = new T.Raycaster(); this._mouse = new T.Vector2(); this._clock = new T.Clock(); // Bloom post-processing this._initBloom(); // Click/mousemove event listeners for interaction const dom = this._renderer.domElement; dom.style.pointerEvents = 'auto'; dom.addEventListener('click', (event) => { if (!this._raycaster || !this._camera || this._clickableObjects.length === 0) return; const rect = dom.getBoundingClientRect(); this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; this._raycaster.setFromCamera(this._mouse, this._camera); const meshes = this._clickableObjects.map(id => { const obj = this._objects.get(id); return obj ? obj.mesh : null; }).filter(Boolean); const intersects = this._raycaster.intersectObjects(meshes, true); if (intersects.length > 0) { // Walk up to find the tracked object let hit = intersects[0].object; const entry = this._objects.find(o => o.mesh === hit); if (entry && this._clickHandlers[entry.id]) { this.terminal.executeCommand(this._clickHandlers[entry.id]); } } }); dom.addEventListener('mousemove', (event) => { if (!this._raycaster || !this._camera || Object.keys(this._hoverHandlers).length === 0) return; const rect = dom.getBoundingClientRect(); this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; this._raycaster.setFromCamera(this._mouse, this._camera); const meshes = this._objects.filter(o => this._hoverHandlers[o.id]).map(o => o.mesh).filter(Boolean); const intersects = this._raycaster.intersectObjects(meshes, true); // Reset all hovered objects to leave color for (const [hid, h] of Object.entries(this._hoverHandlers)) { const ho = this._objects.get(hid); if (ho && ho.mesh && ho.mesh.material && ho.mesh.material.color) { ho.mesh.material.color.setHex(h.origColor != null ? h.origColor : 0x818cf8); } } if (intersects.length > 0) { let hit = intersects[0].object; const entry = this._objects.find(o => o.mesh === hit); if (entry && this._hoverHandlers[entry.id]) { try { hit.material.color.set(this._hoverHandlers[entry.id].enterColor); } catch (e) {} } } }); this._objects = new Map(); this._idCounter = 0; this._animations = {}; this._clickHandlers = {}; this._hoverHandlers = {}; this._followTargets = {}; this._paths = {}; this._pathProgress = {}; this._clickableObjects = []; this._avatar = null; this._speechBubble = null; } async _initTransformControls() { try { if (typeof THREE.TransformControls === 'undefined') { await this._loadScript('downloads/TransformControls.js'); } const T = this._THREE; this._transformControls = new T.TransformControls(this._camera, this._renderer.domElement); this._transformControls.addEventListener('dragging-changed', (event) => { this._controls.enabled = !event.value; }); this._scene.add(this._transformControls); } catch (e) { this.terminal?.print('TransformControls not available: ' + e.message, 'warning'); this._transformControls = null; } } async _initBloom() { try { if (typeof THREE.EffectComposer === 'undefined') { await this._loadScript('downloads/CopyShader.js'); await this._loadScript('downloads/EffectComposer.js'); await this._loadScript('downloads/ShaderPass.js'); await this._loadScript('downloads/RenderPass.js'); await this._loadScript('downloads/LuminosityHighPassShader.js'); await this._loadScript('downloads/UnrealBloomPass.js'); } const T = this._THREE; this._composer = new T.EffectComposer(this._renderer); this._composer.addPass(new T.RenderPass(this._scene, this._camera)); const bloomPass = new T.UnrealBloomPass( new T.Vector2(window.innerWidth, window.innerHeight), 0.2, 0.1, 0.05 ); this._composer.addPass(bloomPass); } catch (e) { this.terminal?.print('Bloom not available: ' + e.message, 'warning'); this._composer = null; } } _startLoop() { if (this._animId) cancelAnimationFrame(this._animId); this._animTime = 0; const loop = () => { this._animId = requestAnimationFrame(loop); const delta = Math.min(this._clock ? this._clock.getDelta() : 0.016, 0.1); this._animTime += delta; if (this._controls) this._controls.update(); // ── Animations ── for (const [id, anim] of Object.entries(this._animations)) { if (!anim.active) continue; const obj = this._objects.get(id); if (!obj || !obj.mesh) continue; const m = obj.mesh; switch (anim.type) { case 'rotate': m.rotation.y += anim.speed * delta * 2; break; case 'bounce': m.position.y = anim.baseY + Math.sin(this._animTime * anim.speed * 2) * anim.height; break; case 'float': m.position.y = anim.baseY + Math.sin(this._animTime * anim.speed * 2) * anim.height; m.rotation.z = Math.sin(this._animTime * anim.speed) * 0.1; break; case 'ease': { const t = (this._animTime * anim.speed) % 1; const e = this._easeInOut(t); m.position.y = anim.baseY + e * anim.height; break; } case 'elastic': { const t = (this._animTime * anim.speed) % 1; const e = this._easeElastic(t); m.position.y = anim.baseY + e * anim.height; break; } case 'bounce-ease': { const t = (this._animTime * anim.speed) % 1; const e = this._easeBounce(t); m.position.y = anim.baseY + e * anim.height; break; } case 'orbit': { const d = anim; d.angle = (d.angle || 0) + d.speed * delta; m.position.x = Math.cos(d.angle) * (d.orbit || 1); m.position.z = Math.sin(d.angle) * (d.orbit || 1); break; } } } // ── Follow targets ── for (const [id, targetId] of Object.entries(this._followTargets)) { const obj = this._objects.get(id); const target = this._objects.get(targetId); if (obj && target && obj.mesh && target.mesh) { obj.mesh.position.lerp(target.mesh.position, 0.05); } } // ── Path following ── for (const [id, path] of Object.entries(this._paths)) { const obj = this._objects.get(id); if (!obj || !obj.mesh || path.length < 2) continue; const prog = this._pathProgress[id] || 0; const next = Math.min(prog + 1, path.length - 1); const p0 = path[prog]; const p1 = path[next]; const t = 0.05; obj.mesh.position.x += (p0.x - obj.mesh.position.x) * t; obj.mesh.position.y += (p0.y - obj.mesh.position.y) * t; obj.mesh.position.z += (p0.z - obj.mesh.position.z) * t; const dist = obj.mesh.position.distanceTo(new T.Vector3(p0.x, p0.y, p0.z)); if (dist < 0.1) { this._pathProgress[id] = next; if (next >= path.length - 1) this._pathProgress[id] = 0; // loop } } // ── Avatar idle animation ── if (this._avatar && this._avatarMood !== 'talking') { this._avatar.position.y += Math.sin(this._animTime * 0.2) * 0.002; } // ── Timeline ── if (this._timelinePlaying) { this._timelineTime += delta; for (const key of this._timeline) { if (key.time <= this._timelineTime && !key._applied) { key._applied = true; const obj = this._objects.get(key.id); if (obj && obj.mesh) { if (key.pos) obj.mesh.position.set(key.pos[0], key.pos[1], key.pos[2]); if (key.rot) obj.mesh.rotation.set(key.rot[0], key.rot[1], key.rot[2]); if (key.scale) obj.mesh.scale.set(key.scale[0], key.scale[1], key.scale[2]); } } } // Check if timeline ended const lastTime = this._timeline.length > 0 ? this._timeline[this._timeline.length - 1].time : 0; if (this._timelineTime > lastTime) { this._timelinePlaying = false; } } if (this._renderer && this._scene && this._camera) { if (this._composer && this._bloomEnabled) { this._composer.render(); } else { this._renderer.render(this._scene, this._camera); } } }; loop(); } _stopLoop() { if (this._animId) { cancelAnimationFrame(this._animId); this._animId = null; } } // ── Lifecycle ── destroy() { this._stopLoop(); this._hideContainer(); 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; } this._disposeAllObjects(); 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; this._importCache = {}; } _disposeAllObjects() { for (const obj of this._objects.values()) { 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 = new Map(); 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.set(id, { mesh, id, label: 'Box ' + id }); this._pushUndo('add', { 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.set(id, { mesh, id, label: 'Sphere ' + id }); this._pushUndo('add', { mesh, id, label: 'Sphere ' + id }); this.terminal.print(`Added sphere (${id}) at origin`, 'success'); return id; } _addCylinder(args) { const T = this._THREE; const radiusTop = parseFloat(args[0]) || 0.5; const radiusBottom = parseFloat(args[1]) || 0.5; const height = parseFloat(args[2]) || 1.5; const color = args[3] || '#fbbf24'; const geo = new T.CylinderGeometry(radiusTop, radiusBottom, height, 24); const mat = new T.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.5 }); const mesh = new T.Mesh(geo, mat); mesh.castShadow = true; const id = this._genId(); this._scene.add(mesh); this._objects.set(id, { mesh, id, label: 'Cylinder ' + id }); this._pushUndo('add', { mesh, id, label: 'Cylinder ' + id }); this.terminal.print(`Added cylinder (${id}) at origin`, 'success'); return id; } _addCone(args) { const T = this._THREE; const radius = parseFloat(args[0]) || 0.6; const height = parseFloat(args[1]) || 1.5; const color = args[2] || '#fb7185'; const geo = new T.ConeGeometry(radius, height, 24); const mat = new T.MeshStandardMaterial({ color, metalness: 0.2, roughness: 0.5 }); const mesh = new T.Mesh(geo, mat); mesh.castShadow = true; const id = this._genId(); this._scene.add(mesh); this._objects.set(id, { mesh, id, label: 'Cone ' + id }); this._pushUndo('add', { mesh, id, label: 'Cone ' + id }); this.terminal.print(`Added cone (${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.set(id, { mesh, id, label: 'Torus ' + id }); this._pushUndo('add', { mesh, id, label: 'Torus ' + id }); this.terminal.print(`Added torus (${id}) at origin`, 'success'); return id; } _addTorusKnot(args) { const T = this._THREE; const radius = parseFloat(args[0]) || 0.8; const tube = parseFloat(args[1]) || 0.25; const color = args[2] || '#22d3ee'; const geo = new T.TorusKnotGeometry(radius, tube, 64, 8, 2, 3); const mat = new T.MeshStandardMaterial({ color, metalness: 0.5, roughness: 0.2 }); const mesh = new T.Mesh(geo, mat); mesh.castShadow = true; const id = this._genId(); this._scene.add(mesh); this._objects.set(id, { mesh, id, label: 'TorusKnot ' + id }); this._pushUndo('add', { mesh, id, label: 'TorusKnot ' + id }); this.terminal.print(`Added torus knot (${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.set(id, { mesh: sprite, id, label: 'Text: ' + text }); this._pushUndo('add', { 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.set(id, { mesh: points, id, label: 'Particles (' + count + ')' }); this._pushUndo('add', { 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.set(id, { mesh: line, id, label: 'Line ' + id }); this._pushUndo('add', { mesh: line, id, label: 'Line ' + id }); this.terminal.print(`Added line (${id})`, 'success'); return id; } // ── Transforms ── _setPosition(id, x, y, z) { const obj = this._objects.get(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.get(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'); } _setScale(id, sx, sy, sz) { const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); obj.mesh.scale.set(sx, sy, sz); this.terminal.print(`Scaled ${id} to (${sx}, ${sy}, ${sz})`, 'success'); } _setColor(id, colorHex) { const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); if (!obj.mesh.material || !obj.mesh.material.color) return this.terminal.print('Object has no color property', 'warning'); const T = this._THREE; try { obj.mesh.material.color.set(colorHex); this.terminal.print(`Set ${id} color to ${colorHex}`, 'success'); } catch (e) { this.terminal.print('Invalid color: ' + colorHex, 'warning'); } } _setMaterial(id, key, value) { const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); if (!obj.mesh.material) return this.terminal.print('Object has no material', 'warning'); const validKeys = ['metalness', 'roughness', 'opacity', 'emissiveIntensity']; if (!validKeys.includes(key)) return this.terminal.print('Valid keys: ' + validKeys.join(', '), 'warning'); const num = parseFloat(value); if (isNaN(num) || num < 0 || num > 1) return this.terminal.print('Value must be 0-1', 'warning'); obj.mesh.material[key] = num; if (key === 'opacity') { obj.mesh.material.transparent = num < 1; } obj.mesh.material.needsUpdate = true; this.terminal.print(`Set ${id} ${key} to ${num}`, 'success'); } _duplicateObject(id) { const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); const T = this._THREE; let newMesh; if (obj.mesh.type === 'Line' || obj.mesh.type === 'LineSegments') { const geo = obj.mesh.geometry.clone(); const mat = obj.mesh.material.clone(); newMesh = new T.Line(geo, mat); newMesh.position.copy(obj.mesh.position); newMesh.position.x += 1.5; } else if (obj.mesh.type === 'Sprite') { const mat = obj.mesh.material.clone(); newMesh = new T.Sprite(mat); newMesh.position.copy(obj.mesh.position); newMesh.position.x += 1.5; newMesh.scale.copy(obj.mesh.scale); } else if (obj.mesh.type === 'Points') { const geo = obj.mesh.geometry.clone(); const mat = obj.mesh.material.clone(); newMesh = new T.Points(geo, mat); newMesh.position.copy(obj.mesh.position); newMesh.position.x += 1.5; } else if (obj.mesh.geometry && obj.mesh.material) { const geo = obj.mesh.geometry.clone(); const mat = obj.mesh.material.clone(); newMesh = new T.Mesh(geo, mat); newMesh.position.copy(obj.mesh.position); newMesh.position.x += 1.5; newMesh.rotation.copy(obj.mesh.rotation); newMesh.scale.copy(obj.mesh.scale); newMesh.castShadow = true; } else { return this.terminal.print('Cannot duplicate this object type', 'warning'); } const newId = this._genId(); this._scene.add(newMesh); this._objects.set(newId, { mesh: newMesh, id: newId, label: obj.label + ' (copy)' }); this.terminal.print(`Duplicated ${id} → ${newId}`, 'success'); return newId; } _groupObjects(ids) { const T = this._THREE; const group = new T.Group(); const moved = []; for (const id of ids) { if (!this._objects.has(id)) { this.terminal.print('Object not found: ' + id, 'warning'); continue; } const obj = this._objects.get(id); this._scene.remove(obj.mesh); group.add(obj.mesh); this._objects.delete(id); moved.push(id); } this._scene.add(group); const groupId = this._genId(); this._objects.set(groupId, { mesh: group, id: groupId, label: 'Group (' + moved.join(',') + ')', _isGroup: true }); this.terminal.print(`Grouped ${moved.length} objects as ${groupId}`, 'success'); return groupId; } _ungroupObject(id) { const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); if (!obj._isGroup) return this.terminal.print('Not a group: ' + id, 'warning'); const T = this._THREE; const group = obj.mesh; const children = [...group.children]; const newIds = []; for (const child of children) { group.remove(child); this._scene.add(child); const newId = this._genId(); this._objects.set(newId, { mesh: child, id: newId, label: 'Ungrouped ' + newId }); newIds.push(newId); } this._scene.remove(group); this._objects.delete(id); this.terminal.print(`Ungrouped ${id} → ${newIds.length} objects`, 'success'); return newIds; } // ── Animations ── _pushUndo(action, data) { this._undoStack.push({ action, data, timestamp: Date.now() }); if (this._undoStack.length > 50) this._undoStack.shift(); this._redoStack = []; } _undo() { if (this._undoStack.length === 0) return this.terminal.print('Nothing to undo', 'info'); const entry = this._undoStack.pop(); this._redoStack.push(entry); if (entry.action === 'remove') { // Re-add the object const obj = entry.data; this._scene.add(obj.mesh); this._objects.set(obj.id, obj); this.terminal.print('Undo: restored ' + obj.id, 'success'); } else if (entry.action === 'add') { // Remove the object const obj = entry.data; this._scene.remove(obj.mesh); this._objects.delete(obj.id); this.terminal.print('Undo: removed ' + obj.id, 'success'); } else if (entry.action === 'clear') { // Restore all objects for (const obj of entry.data) { this._scene.add(obj.mesh); this._objects.set(obj.id, obj); } this.terminal.print('Undo: restored scene', 'success'); } else if (entry.action === 'transform') { const { id, pos, rot, scale } = entry.data; const obj = this._objects.get(id); if (obj) { if (pos) obj.mesh.position.set(pos[0], pos[1], pos[2]); if (rot) obj.mesh.rotation.set(rot[0], rot[1], rot[2]); if (scale) obj.mesh.scale.set(scale[0], scale[1], scale[2]); this.terminal.print('Undo: reverted ' + id, 'success'); } } } _redo() { if (this._redoStack.length === 0) return this.terminal.print('Nothing to redo', 'info'); const entry = this._redoStack.pop(); this._undoStack.push(entry); if (entry.action === 'add') { const obj = entry.data; this._scene.add(obj.mesh); this._objects.set(obj.id, obj); this.terminal.print('Redo: restored ' + obj.id, 'success'); } else if (entry.action === 'remove') { const obj = entry.data; this._scene.remove(obj.mesh); this._objects.delete(obj.id); this.terminal.print('Redo: removed ' + obj.id, 'success'); } else if (entry.action === 'clear') { for (const obj of entry.data) { this._scene.remove(obj.mesh); this._objects.delete(obj.id); } this.terminal.print('Redo: cleared scene', 'success'); } } _renameObject(id, newName) { const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); obj.label = newName; this.terminal.print(`Renamed ${id} to "${newName}"`, 'success'); } _selectObject(id) { if (!this._transformControls) return this.terminal.print('TransformControls not loaded', 'warning'); if (!id) { this._transformControls.detach(); this._selectedId = null; this.terminal.print('Deselected', 'info'); return; } const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); this._transformControls.attach(obj.mesh); this._selectedId = id; this.terminal.print('Selected ' + id, 'success'); } // Easing functions _easeInOut(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; } _easeOut(t) { return 1 - Math.pow(1 - t, 3); } _easeElastic(t) { return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1; } _easeBounce(t) { const n1 = 7.5625, d1 = 2.75; if (t < 1 / d1) return n1 * t * t; else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75; else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375; else return n1 * (t -= 2.625 / d1) * t + 0.984375; } _animateObject(args) { if (args.length < 2) return this.terminal.print('Usage: three animate rotate|bounce|float|orbit|ease|elastic|bounce-ease [params] [easing]', 'warning'); const id = args[0]; const type = args[1]; const easing = args[args.length - 1] === 'ease' || args[args.length - 1] === 'elastic' || args[args.length - 1] === 'bounce' ? args.pop() : 'linear'; const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); const anim = { type, speed: 1, active: true, baseY: obj.mesh.position.y, height: 0.5, angle: 0, orbit: 2, easing }; if (type === 'orbit') { anim.orbit = parseFloat(args[2]) || 2; anim.speed = parseFloat(args[3]) || 0.5; } else if (type === 'bounce' || type === 'float') { anim.height = parseFloat(args[2]) || 0.5; anim.speed = parseFloat(args[3]) || 1; } else { anim.speed = parseFloat(args[2]) || 1; } this._animations[id] = anim; this.terminal.print(`▶️ Animated ${id}: ${type} (speed: ${anim.speed})`, 'success'); } _stopAnimation(args) { const id = args[0]; if (!id) { this._animations = {}; this.terminal.print('All animations stopped', 'info'); return; } if (this._animations[id]) { delete this._animations[id]; this.terminal.print(`Stopped animation for ${id}`, 'info'); } else { this.terminal.print('No animation for ' + id, 'warning'); } } // ── Timeline ── _timelineCommand(args) { if (args.length === 0) return this.terminal.print('Usage: three timeline add|play|pause|stop|clear|list', 'warning'); const sub = args[0]; const rest = args.slice(1); switch (sub) { case 'add': { if (rest.length < 5) return this.terminal.print('Usage: three timeline add ', 'warning'); const id = rest[0]; const time = parseFloat(rest[1]); if (isNaN(time)) return this.terminal.print('Time must be a number', 'warning'); const x = parseFloat(rest[2]), y = parseFloat(rest[3]), z = parseFloat(rest[4]); this._timeline.push({ id, time, pos: [x, y, z], _applied: false }); this._timeline.sort((a, b) => a.time - b.time); this.terminal.print(`Keyframe added: ${id} @ ${time}s → (${x}, ${y}, ${z})`, 'success'); break; } case 'play': this._timeline.forEach(k => k._applied = false); this._timelinePlaying = true; this._timelineTime = 0; this.terminal.print('▶️ Timeline playing', 'success'); break; case 'pause': this._timelinePlaying = false; this.terminal.print('⏸ Timeline paused', 'info'); break; case 'stop': this._timelinePlaying = false; this._timelineTime = 0; this._timeline.forEach(k => k._applied = false); this.terminal.print('⏹ Timeline stopped', 'info'); break; case 'clear': this._timeline = []; this._timelinePlaying = false; this._timelineTime = 0; this.terminal.print('Timeline cleared', 'info'); break; case 'list': if (this._timeline.length === 0) return this.terminal.print('No keyframes', 'info'); this.terminal.print('=== Timeline (' + this._timeline.length + ' keyframes) ===', 'header'); for (const k of this._timeline) { this.terminal.print(` ${k.id} @ ${k.time}s → (${k.pos[0]}, ${k.pos[1]}, ${k.pos[2]})`, 'info'); } break; default: this.terminal.print('Unknown: ' + sub + '. Try: add, play, pause, stop, clear, list', 'warning'); } } // ── Interaction ── _setClickHandler(args) { if (args.length < 2) return this.terminal.print('Usage: three click ', 'warning'); const id = args[0]; const cmd = args.slice(1).join(' '); const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); this._clickHandlers[id] = cmd; if (!this._clickableObjects.includes(id)) this._clickableObjects.push(id); this.terminal.print(`Click handler set: ${id} → "${cmd}"`, 'success'); } _setHoverHandler(args) { if (args.length < 3) return this.terminal.print('Usage: three hover ', 'warning'); const id = args[0]; const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); this._hoverHandlers[id] = { enterColor: args[1], leaveColor: args[2], origColor: obj.mesh.material && obj.mesh.material.color ? obj.mesh.material.color.getHex() : null }; this.terminal.print(`Hover set: ${id}`, 'success'); } _setFollow(id, targetId) { const obj = this._objects.get(id); const target = this._objects.get(targetId); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); if (!target) return this.terminal.print('Target not found: ' + targetId, 'error'); this._followTargets[id] = targetId; this.terminal.print(`${id} now follows ${targetId}`, 'success'); } _unfollow(id) { if (this._followTargets[id]) { delete this._followTargets[id]; this.terminal.print(`${id} stopped following`, 'info'); } else { this.terminal.print('Not following: ' + id, 'warning'); } } _setPath(args) { if (args.length < 4) return this.terminal.print('Usage: three path [more points...]', 'warning'); const id = args[0]; const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); const coords = args.slice(1).map(parseFloat); const points = []; for (let i = 0; i < coords.length; i += 3) { if (coords.length - i < 3) break; points.push({ x: coords[i], y: coords[i+1], z: coords[i+2] }); } if (points.length < 2) return this.terminal.print('Need at least 2 points (6 coordinates)', 'warning'); this._paths[id] = points; this._pathProgress[id] = 0; this.terminal.print(`Path set for ${id}: ${points.length} waypoints`, 'success'); } // ── Avatars ── _createAvatar(args) { const T = this._THREE; const name = args[0] || 'assistant'; const color = args[1] || '#00ff88'; // Remove existing avatar if (this._avatar) { this._scene.remove(this._avatar); this._avatar = null; } if (this._speechBubble) { this._scene.remove(this._speechBubble); this._speechBubble = null; } const group = new T.Group(); // Body (rounded cylinder) const bodyGeo = new T.CylinderGeometry(0.6, 0.7, 1.2, 12); const bodyMat = new T.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.4, emissive: color, emissiveIntensity: 0.1 }); const body = new T.Mesh(bodyGeo, bodyMat); body.position.y = 0.6; body.castShadow = true; group.add(body); // Head (sphere) const headGeo = new T.SphereGeometry(0.4, 16, 16); const headMat = new T.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.1, roughness: 0.3 }); const head = new T.Mesh(headGeo, headMat); head.position.y = 1.4; head.castShadow = true; group.add(head); // Eyes (two small spheres) const eyeMat = new T.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 }); const eyeGeo = new T.SphereGeometry(0.08, 8, 8); const leftEye = new T.Mesh(eyeGeo, eyeMat); leftEye.position.set(-0.15, 1.5, 0.35); group.add(leftEye); const rightEye = new T.Mesh(eyeGeo, eyeMat); rightEye.position.set(0.15, 1.5, 0.35); group.add(rightEye); // Arms (thin cylinders) const armMat = new T.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.2, roughness: 0.5 }); const armGeo = new T.CylinderGeometry(0.06, 0.06, 0.6, 6); const leftArm = new T.Mesh(armGeo, armMat); leftArm.position.set(-0.7, 0.8, 0); leftArm.rotation.z = 0.2; group.add(leftArm); const rightArm = new T.Mesh(armGeo, armMat); rightArm.position.set(0.7, 0.8, 0); rightArm.rotation.z = -0.2; group.add(rightArm); group.position.set(0, 0, 0); this._scene.add(group); this._avatar = group; this._avatarMood = 'neutral'; // Store body parts for animation group.userData = { body, head, leftEye, rightEye, leftArm, rightArm, bodyMat, headMat }; this.terminal.print(`🤖 Avatar "${name}" created`, 'success'); return group; } _avatarPose(pose) { if (!this._avatar) return this.terminal.print('No avatar. Create one with three avatar ', 'warning'); const parts = this._avatar.userData; if (!parts) return; switch (pose) { case 'idle': parts.leftArm.rotation.z = 0.2; parts.rightArm.rotation.z = -0.2; break; case 'wave': parts.rightArm.rotation.z = -1.5; break; case 'think': parts.leftArm.rotation.z = 0.8; parts.rightArm.rotation.z = -0.8; parts.head.rotation.x = -0.2; break; case 'point': parts.rightArm.rotation.z = -0.5; parts.rightArm.rotation.x = -0.8; break; case 'cross': parts.leftArm.rotation.z = 0.8; parts.rightArm.rotation.z = -0.8; break; default: this.terminal.print('Poses: idle, wave, think, point, cross', 'warning'); } } _avatarLookAt(x, y, z) { if (!this._avatar) return this.terminal.print('No avatar', 'warning'); const parts = this._avatar.userData; if (!parts || !parts.head) return; const dx = x - this._avatar.position.x; const dy = y - (this._avatar.position.y + 1.4); const dz = z - this._avatar.position.z; parts.head.rotation.x = Math.atan2(dy, Math.sqrt(dx*dx + dz*dz)) * 0.5; parts.head.rotation.y = Math.atan2(dx, dz) * 0.5; } _avatarSpeak(text) { if (!this._avatar) return this.terminal.print('No avatar', 'warning'); const T = this._THREE; // Remove old speech bubble if (this._speechBubble) { if (this._speechBubble.material) { if (this._speechBubble.material.map) this._speechBubble.material.map.dispose(); this._speechBubble.material.dispose(); } this._speechBubble.removeFromParent(); this._speechBubble = null; } if (this._speechTimeout) clearTimeout(this._speechTimeout); const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 128; const ctx = canvas.getContext('2d'); ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.roundRect ? ctx.roundRect(10, 10, 492, 108, 16) : ctx.fillRect(10, 10, 492, 108); ctx.fill(); ctx.font = '24px monospace'; ctx.fillStyle = '#00ff88'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const lines = this._wrapText(ctx, text, 450); lines.forEach((line, i) => { ctx.fillText(line, 256, 40 + i * 30); }); const texture = new T.CanvasTexture(canvas); const mat = new T.SpriteMaterial({ map: texture, transparent: true, depthTest: false }); const sprite = new T.Sprite(mat); sprite.position.set(0, 2.5, 0); sprite.scale.set(3, 0.75, 1); this._avatar.add(sprite); this._speechBubble = sprite; this._avatarMood = 'talking'; // Auto-clear talking state after text duration setTimeout(() => { if (this._avatarMood === 'talking') this._avatarMood = 'neutral'; }, Math.max(2000, text.length * 100)); } _wrapText(ctx, text, maxWidth) { const words = text.split(' '); const lines = []; let line = ''; for (const word of words) { const test = line ? line + ' ' + word : word; if (ctx.measureText(test).width > maxWidth && line) { lines.push(line); line = word; } else { line = test; } } if (line) lines.push(line); return lines.length > 0 ? lines : ['']; } _avatarEmotion(emotion) { if (!this._avatar) return this.terminal.print('No avatar', 'warning'); const parts = this._avatar.userData; if (!parts) return; this._avatarMood = emotion; const colors = { happy: 0x00ff88, sad: 0x4488ff, angry: 0xff4444, confused: 0xffaa00, thinking: 0xaa88ff, neutral: 0x00ff88 }; const color = colors[emotion] || 0x00ff88; if (parts.bodyMat) { parts.bodyMat.color.setHex(color); parts.bodyMat.emissive.setHex(color); } if (parts.leftEye && parts.rightEye) { parts.leftEye.material.color.setHex(color); parts.rightEye.material.color.setHex(color); } this.terminal.print(`Avatar emotion: ${emotion}`, '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); 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); const _mmid = this._genId(); this._objects.set(_mmid, { mesh, id: _mmid, 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); const _slid = this._genId(); this._objects.set(_slid, { mesh: sprite, id: _slid, 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); const _eid = this._genId(); this._objects.set(_eid, { mesh: line, id: _eid, label: 'Edge' }); } // Auto-frame camera const nodeMeshes = Array.from(this._objects.values()).filter(o => o.mesh.type === 'Mesh').map(o => o.mesh); if (nodeMeshes.length > 0) { const box = new T.Box3(); for (const m of nodeMeshes) box.expandByObject(m); 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); const _sid = this._genId(); this._objects.set(_sid, { mesh: sun, id: _sid, 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); const _pid = this._genId(); this._objects.set(_pid, { mesh, id: _pid, label: p.name }); this._animations[_pid] = { type: 'orbit', speed: p.speed, active: true, angle: Math.random() * Math.PI * 2, orbit: p.orbit }; 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); const _rid = this._genId(); this._objects.set(_rid, { mesh: ring, id: _rid, 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; const count = (size * 2 + 1) ** 2; const sharedGeo = new T.BoxGeometry(0.3, 0.3, 0.3); const sharedMat = new T.MeshStandardMaterial({ emissiveIntensity: 0.1 }); const instancedMesh = new T.InstancedMesh(sharedGeo, sharedMat, count); const dummy = new T.Object3D(); const color = new T.Color(); let idx = 0; for (let x = -size; x <= size; x++) { for (let z = -size; z <= size; z++) { const hue = ((x + size) / (size * 2)) * 0.6 + 0.5; color.setHSL(hue, 0.8, 0.5); dummy.position.set(x * spacing, 0, z * spacing); dummy.updateMatrix(); instancedMesh.setMatrixAt(idx, dummy.matrix); instancedMesh.setColorAt(idx, color); idx++; } } instancedMesh.instanceMatrix.needsUpdate = true; instancedMesh.instanceColor.needsUpdate = true; instancedMesh.castShadow = true; this._scene.add(instancedMesh); const _gid = this._genId(); this._objects.set(_gid, { mesh: instancedMesh, id: _gid, label: 'Grid' }); this.terminal.print(`📐 Grid preset: ${count} cubes (1 draw call)`, 'success'); } _presetParticleField(args) { if (!this._scene) this._createScene(); this._showContainer(this._mode); this._clearScene(); const T = this._THREE; const count = parseInt(args[0]) || 2000; const spread = parseFloat(args[1]) || 30; const geo = new T.BufferGeometry(); const positions = new Float32Array(count * 3); const colors = new Float32Array(count * 3); for (let i = 0; i < count; i++) { positions[i*3] = (Math.random() - 0.5) * spread; positions[i*3+1] = (Math.random() - 0.5) * spread * 0.5; positions[i*3+2] = (Math.random() - 0.5) * spread; const c = new T.Color().setHSL(Math.random() * 0.3 + 0.4, 0.8, 0.5); colors[i*3] = c.r; colors[i*3+1] = c.g; colors[i*3+2] = c.b; } geo.setAttribute('position', new T.BufferAttribute(positions, 3)); geo.setAttribute('color', new T.BufferAttribute(colors, 3)); const mat = new T.PointsMaterial({ size: 0.2, vertexColors: true, transparent: true, opacity: 0.8, blending: T.AdditiveBlending, sizeAttenuation: true }); const points = new T.Points(geo, mat); this._scene.add(points); const _pfid = this._genId(); this._objects.set(_pfid, { mesh: points, id: _pfid, label: 'Particle Field' }); this.terminal.print(`✨ Particle field: ${count} stars`, 'success'); } // ── Import ── async _importModel(args) { if (!args[0]) return this.terminal.print('Usage: three import ', 'warning'); const url = args[0]; // Check cache if (this._importCache[url]) { const cached = this._importCache[url]; const newMesh = cached.clone(); this._scene.add(newMesh); const id = this._genId(); this._objects.set(id, { mesh: newMesh, id, label: 'Imported: ' + url.split('/').pop() }); this.terminal.print(`Loaded cached model (${id})`, 'success'); return id; } this.terminal.print('📥 Loading model from ' + url + '...', 'info'); try { // Load GLTFLoader if not already loaded if (typeof THREE.GLTFLoader === 'undefined') { await this._loadScript('downloads/GLTFLoader.js'); } const T = this._THREE; const loader = new T.GLTFLoader(); const gltf = await new Promise((resolve, reject) => { loader.load(url, resolve, undefined, reject); }); const model = gltf.scene || gltf; if (!this._scene) this._createScene(); if (!this._visible) this._showContainer(this._mode); this._scene.add(model); const id = this._genId(); this._objects.set(id, { mesh: model, id, label: 'Imported: ' + url.split('/').pop() }); this._importCache[url] = model; this.terminal.print('📦 Model imported (' + id + ')', 'success'); return id; } catch (e) { this.terminal.print('❌ Import failed: ' + e.message, 'error'); return null; } } // ── 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(/[^a-zA-Z0-9_-]/g, '_'); const T = this._THREE; const objects = []; for (const obj of this._objects.values()) { 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; const savedObjects = Array.from(this._objects.values()); this._pushUndo('clear', savedObjects); this._disposeAllObjects(); this._animations = {}; this._clickHandlers = {}; this._hoverHandlers = {}; this._followTargets = {}; this._paths = {}; this._pathProgress = {}; this._clickableObjects = []; this._timeline = []; this._timelinePlaying = false; this._timelineTime = 0; if (this._avatar) { this._avatar.traverse((child) => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (child.material.map) child.material.map.dispose(); child.material.dispose(); } }); this._scene.remove(this._avatar); this._avatar = null; } if (this._speechBubble) { if (this._speechBubble.material) { if (this._speechBubble.material.map) this._speechBubble.material.map.dispose(); this._speechBubble.material.dispose(); } this._speechBubble = null; } if (this._speechTimeout) clearTimeout(this._speechTimeout); this._speechTimeout = null; } _listObjects() { if (this._objects.size === 0) return this.terminal.print('No objects in scene', 'info'); this.terminal.print(`=== 3D Objects (${this._objects.size}) ===`, 'header'); for (const obj of this._objects.values()) { const pos = obj.mesh.position; const anim = this._animations[obj.id] ? ' ▶️' : ''; const click = this._clickHandlers[obj.id] ? ' 👆' : ''; const follow = this._followTargets[obj.id] ? ' 🔗' : ''; this.terminal.print(` ${obj.id} ${obj.label} @ (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})${anim}${click}${follow}`); } } _removeObject(id) { const obj = this._objects.get(id); if (!obj) return this.terminal.print('Object not found: ' + id, 'error'); 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.delete(id); delete this._animations[id]; delete this._clickHandlers[id]; delete this._hoverHandlers[id]; delete this._followTargets[id]; delete this._paths[id]; delete this._pathProgress[id]; this._clickableObjects = this._clickableObjects.filter(c => c !== id); this.terminal.print(`Removed ${id}`, 'success'); } _setBgColor(args) { if (!args[0]) return this.terminal.print('Usage: three bgcolor (e.g. #0a0a14)', 'warning'); const colorStr = String(args[0] || '').replace('#', ''); const color = parseInt(colorStr, 16); if (isNaN(color) || colorStr.length < 3) 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'); } _takeSnapshot() { if (!this._renderer || !this._scene) return this.terminal.print('No scene to capture', 'warning'); try { this._renderer.render(this._scene, this._camera); const dataUrl = this._renderer.domElement.toDataURL('image/png'); const link = document.createElement('a'); link.download = 'three-snapshot-' + Date.now() + '.png'; link.href = dataUrl; link.click(); this.terminal.print('📸 Snapshot saved', 'success'); } catch (e) { this.terminal.print('Snapshot failed: ' + e.message, 'error'); } } // ── 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|cylinder|cone|torus|torusknot|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 'cylinder': this._addCylinder(params); break; case 'cone': this._addCone(params); break; case 'torus': this._addTorus(params); break; case 'torusknot': this._addTorusKnot(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, cylinder, cone, torus, torusknot, 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'); const cx = parseFloat(rest[0]), cy = parseFloat(rest[1]), cz = parseFloat(rest[2]); if ([cx, cy, cz].some(v => isNaN(v))) return this.terminal.print('Invalid number in camera position', 'warning'); this._camera.position.set(cx, cy, cz); 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 'pos': { if (rest.length < 4) return this.terminal.print('Usage: three pos ', 'warning'); const px = parseFloat(rest[1]), py = parseFloat(rest[2]), pz = parseFloat(rest[3]); if ([px, py, pz].some(v => isNaN(v))) return this.terminal.print('Invalid number in position', 'warning'); this._setPosition(rest[0], px, py, pz); break; } case 'rotate': { if (rest.length < 4) return this.terminal.print('Usage: three rotate (radians)', 'warning'); const rx = parseFloat(rest[1]), ry = parseFloat(rest[2]), rz = parseFloat(rest[3]); if ([rx, ry, rz].some(v => isNaN(v))) return this.terminal.print('Invalid number in rotation', 'warning'); this._setRotation(rest[0], rx, ry, rz); break; } case 'scale': { if (rest.length < 4) return this.terminal.print('Usage: three scale ', 'warning'); const sx = parseFloat(rest[1]), sy = parseFloat(rest[2]), sz = parseFloat(rest[3]); if ([sx, sy, sz].some(v => isNaN(v))) return this.terminal.print('Invalid number in scale', 'warning'); this._setScale(rest[0], sx, sy, sz); break; } case 'color': { if (rest.length < 2) return this.terminal.print('Usage: three color ', 'warning'); this._setColor(rest[0], rest[1]); break; } case 'material': { if (rest.length < 3) return this.terminal.print('Usage: three material ', 'warning'); this._setMaterial(rest[0], rest[1], rest[2]); break; } case 'duplicate': { if (!rest[0]) return this.terminal.print('Usage: three duplicate ', 'warning'); this._duplicateObject(rest[0]); break; } case 'group': { if (rest.length < 2) return this.terminal.print('Usage: three group ...', 'warning'); this._groupObjects(rest); break; } case 'ungroup': { if (!rest[0]) return this.terminal.print('Usage: three ungroup ', 'warning'); this._ungroupObject(rest[0]); break; } case 'animate': { this._animateObject(rest); break; } case 'stop': { this._stopAnimation(rest); break; } case 'timeline': { this._timelineCommand(rest); break; } case 'click': { this._setClickHandler(rest); break; } case 'hover': { this._setHoverHandler(rest); break; } case 'follow': { if (rest.length < 2) return this.terminal.print('Usage: three follow ', 'warning'); this._setFollow(rest[0], rest[1]); break; } case 'unfollow': { if (!rest[0]) return this.terminal.print('Usage: three unfollow ', 'warning'); this._unfollow(rest[0]); break; } case 'path': { this._setPath(rest); break; } case 'avatar': { this._createAvatar(rest); break; } case 'pose': { if (!rest[0]) return this.terminal.print('Usage: three pose idle|wave|think|point|cross', 'warning'); this._avatarPose(rest[0]); break; } case 'look': { if (rest.length < 3) return this.terminal.print('Usage: three look ', 'warning'); this._avatarLookAt(parseFloat(rest[0]), parseFloat(rest[1]), parseFloat(rest[2])); break; } case 'speak': { if (!rest[0]) return this.terminal.print('Usage: three speak ', 'warning'); this._avatarSpeak(rest.join(' ')); break; } case 'emotion': { if (!rest[0]) return this.terminal.print('Usage: three emotion happy|sad|angry|confused|thinking|neutral', 'warning'); this._avatarEmotion(rest[0]); 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 'mindmap': this._renderMindmap(rest); break; case 'export': this._export(rest); break; case 'import': await this._importModel(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 if (preset === 'particles' || preset === 'stars') this._presetParticleField(rest.slice(1)); else this.terminal.print('Presets: solar, grid, particles', 'warning'); break; } case 'bgcolor': this._setBgColor(rest); break; case 'bloom': this._bloomEnabled = !this._bloomEnabled; this.terminal.print('Bloom ' + (this._bloomEnabled ? 'ON' : 'OFF'), 'success'); break; case 'select': this._selectObject(rest[0]); break; case 'undo': this._undo(); break; case 'redo': this._redo(); break; case 'rename': if (rest.length < 2) return this.terminal.print('Usage: three rename ', 'warning'); this._renameObject(rest[0], rest.slice(1).join(' ')); break; case 'batch': { const sub = rest[0]; const target = rest[1]; const ids = target === 'all' ? Array.from(this._objects.keys()) : [target]; if (sub === 'color' && rest[2]) { for (const bid of ids) { const bo = this._objects.get(bid); if (bo && bo.mesh.material && bo.mesh.material.color) { bo.mesh.material.color.set(rest[2]); } } this.terminal.print(`Batch color ${rest[2]} applied to ${ids.length} objects`, 'success'); } else if (sub === 'remove') { for (const bid of ids) this._removeObject(bid); this.terminal.print(`Batch removed ${ids.length} objects`, 'success'); } else if (sub === 'animate' && rest[2]) { for (const bid of ids) this._animateObject([bid, rest[2], rest[3] || '1']); this.terminal.print(`Batch animated ${ids.length} objects`, 'success'); } else if (sub === 'stop') { for (const bid of ids) { delete this._animations[bid]; } this.terminal.print(`Batch stopped ${ids.length} objects`, 'success'); } else { this.terminal.print('Usage: three batch color|remove|animate|stop [params]', 'warning'); } break; } case 'gizmo': if (!this._transformControls) return this.terminal.print('TransformControls not loaded', 'warning'); const gmode = rest[0] || 'translate'; if (['translate', 'rotate', 'scale'].includes(gmode)) { this._transformControls.setMode(gmode); this.terminal.print('Gizmo mode: ' + gmode, 'success'); } else { this.terminal.print('Modes: translate, rotate, scale', 'warning'); } break; case 'hide': this._hideContainer(); this.terminal.print('3D view closed. Use three scene or three fullscreen to reopen.', 'info'); break; case 'snapshot': this._takeSnapshot(); break; default: this.terminal.print('Unknown subcommand: ' + sub + '. Try: three help', 'warning'); } } _printHelp() { this.terminal.print('=== Three.js 3D Module (v4.5.86) ===', '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(' three snapshot Save PNG screenshot'); 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 cylinder [rt] [rb] [h] [c] Add a cylinder'); this.terminal.print(' three add cone [r] [h] [c] Add a cone'); this.terminal.print(' three add torus [r] [tube] [c] Add a torus'); this.terminal.print(' three add torusknot [r] [tube] [c] Add a torus knot'); 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 duplicate Clone an object'); this.terminal.print(' three group ... Group objects together'); this.terminal.print(' three ungroup Ungroup a group'); this.terminal.print('', ''); this.terminal.print(' ── Transforms ──', 'info'); this.terminal.print(' three pos Move an object'); this.terminal.print(' three rotate Rotate (radians)'); this.terminal.print(' three scale Scale an object'); this.terminal.print(' three color Change object color'); this.terminal.print(' three material Change material (metalness, roughness, opacity)'); 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(' ── Animations ──', 'info'); this.terminal.print(' three animate rotate|bounce|float|orbit [speed] Animate object'); this.terminal.print(' three stop [id] Stop animation(s)'); this.terminal.print(' three timeline add Add keyframe'); this.terminal.print(' three timeline play|pause|stop|clear|list Control timeline'); this.terminal.print('', ''); this.terminal.print(' ── Interaction ──', 'info'); this.terminal.print(' three click Make object clickable (runs command)'); this.terminal.print(' three hover Hover highlight colors'); this.terminal.print(' three follow Make object follow another'); this.terminal.print(' three unfollow Stop following'); this.terminal.print(' three path [more...] Set waypoint path'); this.terminal.print('', ''); this.terminal.print(' ── Avatars ──', 'info'); this.terminal.print(' three avatar [name] [color] Create an AI avatar'); this.terminal.print(' three pose idle|wave|think|point|cross Set avatar pose'); this.terminal.print(' three look Avatar looks at point'); this.terminal.print(' three speak Show speech bubble'); this.terminal.print(' three emotion happy|sad|angry|confused|thinking|neutral'); 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(' three preset particles [n] [spread] Star field'); this.terminal.print('', ''); this.terminal.print(' ── Import/Export ──', 'info'); this.terminal.print(' three export [name] Export scene as standalone HTML'); this.terminal.print(' three import Import 3D model (GLTF/GLB)'); 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('avatar') || desc.includes('character') || desc.includes('robot')) { this._createAvatar(['assistant', '#00ff88']); results.push('avatar'); } 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) { const id1 = this._addBox(['1', '#818cf8']); const id2 = this._addSphere(['0.6', '#34d399']); this._setPosition(id1, -2, 0, 0); this._setPosition(id2, 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'); }