Professional-grade Tampermonkey userscript for runtime analysis, debugging, exploration, and research of Three.js games and applications.
// ==UserScript==
// @name Insight - Three.js Runtime Analysis Studio
// @namespace https://github.com/insight-tools/insight
// @version 1.0.0
// @description Professional-grade Tampermonkey userscript for runtime analysis, debugging, exploration, and research of Three.js games and applications.
// @author Insight Platform & F1xL1T & FunWithScripts & C00lrXX
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
/**
* =========================================================================
* UTILITIES & HELPERS
* =========================================================================
*/
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(listener);
return () => this.off(event, listener);
}
off(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(l => l !== listener);
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(...args));
}
}
clear() {
this.events = {};
}
}
class Module extends EventEmitter {
constructor(insight) {
super();
this.insight = insight;
this.disposables = [];
}
init() { /* Hook runtime immediately */ }
initUI() { /* Setup UI after DOM load */ }
registerCleanup(fn) {
this.disposables.push(fn);
}
dispose() {
this.disposables.forEach(fn => fn());
this.disposables = [];
this.clear();
}
}
// Basic OBJ Exporter (Solves export feature without relying on external CDNs)
const exportToOBJ = (object3D) => {
let obj = '';
let vertexOffset = 1;
object3D.traverse((child) => {
if (child.isMesh && child.geometry) {
const geo = child.geometry;
const positions = geo.attributes.position;
if (!positions) return;
obj += `o ${child.name || child.type || 'Mesh'}\n`;
const matrix = child.matrixWorld;
const vec3 = new window.THREE.Vector3();
for (let i = 0; i < positions.count; i++) {
vec3.fromBufferAttribute(positions, i).applyMatrix4(matrix);
obj += `v ${vec3.x} ${vec3.y} ${vec3.z}\n`;
}
if (geo.index) {
const indices = geo.index.array;
for (let i = 0; i < indices.length; i += 3) {
obj += `f ${indices[i]+vertexOffset} ${indices[i+1]+vertexOffset} ${indices[i+2]+vertexOffset}\n`;
}
} else {
for (let i = 0; i < positions.count; i += 3) {
obj += `f ${i+vertexOffset} ${i+1+vertexOffset} ${i+2+vertexOffset}\n`;
}
}
vertexOffset += positions.count;
}
});
const blob = new Blob([obj], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${object3D.name || 'Insight_Export'}.obj`;
a.click();
URL.revokeObjectURL(a.href);
};
/**
* =========================================================================
* UI FRAMEWORK & COMPONENTS (With Persistence)
* =========================================================================
*/
class UIWindow {
constructor(ui, id, title, iconName) {
this.ui = ui;
this.id = id;
this.el = document.createElement('div');
this.el.className = 'insight-window';
// Restore window state
const state = JSON.parse(localStorage.getItem(`insight_win_${this.id}`) || 'null');
this.el.style.left = state?.left || '100px';
this.el.style.top = state?.top || '100px';
this.el.style.width = state?.width || 'auto';
this.el.style.height = state?.height || 'auto';
this.el.style.zIndex = '1000';
this.header = document.createElement('div');
this.header.className = 'insight-header';
const iconEl = document.createElement('i');
iconEl.setAttribute('data-lucide', iconName);
iconEl.style.width = '14px';
iconEl.style.height = '14px';
iconEl.style.marginRight = '8px';
const titleEl = document.createElement('span');
titleEl.className = 'title';
titleEl.textContent = title;
const closeBtn = document.createElement('i');
closeBtn.setAttribute('data-lucide', 'x');
closeBtn.className = 'close-btn';
this.header.appendChild(iconEl);
this.header.appendChild(titleEl);
this.header.appendChild(closeBtn);
this.content = document.createElement('div');
this.content.className = 'window-content';
this.el.appendChild(this.header);
this.el.appendChild(this.content);
this.ui.shadowRoot.appendChild(this.el);
this._onCloseCallbacks = [];
closeBtn.addEventListener('click', () => this.close());
this.makeDraggableAndResizable();
this.ui.refreshIcons(this.header);
}
saveState() {
localStorage.setItem(`insight_win_${this.id}`, JSON.stringify({
left: this.el.style.left,
top: this.el.style.top,
width: this.el.style.width,
height: this.el.style.height
}));
}
makeDraggableAndResizable() {
let isDragging = false;
let startX, startY, initialLeft, initialTop;
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
this.el.style.left = `${initialLeft + dx}px`;
this.el.style.top = `${initialTop + dy}px`;
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this.saveState();
};
this.header.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('close-btn')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialLeft = parseInt(this.el.style.left || 0, 10);
initialTop = parseInt(this.el.style.top || 0, 10);
// Bring to front
const allWindows = this.ui.shadowRoot.querySelectorAll('.insight-window');
let maxZ = 1000;
allWindows.forEach(w => {
const z = parseInt(w.style.zIndex || 1000, 10);
if (z > maxZ) maxZ = z;
});
this.el.style.zIndex = maxZ + 1;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
// Save state on resize
new ResizeObserver(() => this.saveState()).observe(this.el);
}
onClose(cb) {
this._onCloseCallbacks.push(cb);
}
close() {
this.el.remove();
this._onCloseCallbacks.forEach(cb => cb());
this._onCloseCallbacks = [];
}
}
class UIFramework {
constructor(insight) {
this.insight = insight;
this.iconsLoaded = false;
}
mount() {
this.host = document.createElement('div');
this.host.id = 'insight-platform-host';
this.host.style.cssText = 'position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 9999999; overflow: visible;';
const target = document.body || document.documentElement;
target.appendChild(this.host);
this.shadowRoot = this.host.attachShadow({ mode: 'open' });
this.injectStyles();
this.loadDependencies();
}
injectStyles() {
const style = document.createElement('style');
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
:host {
--bg-base: #18181B;
--bg-surface: #27272A;
--bg-hover: #3F3F46;
--border: #3F3F46;
--text-main: #F4F4F5;
--text-muted: #A1A1AA;
--accent: #3B82F6;
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
font-family: var(--font-sans);
color: var(--text-main);
font-size: 13px;
}
* { box-sizing: border-box; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bg-hover); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--border); }
.insight-window {
position: absolute;
background: rgba(39, 39, 42, 0.95);
backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
min-width: 320px;
min-height: 200px;
resize: both;
overflow: hidden;
}
.insight-header {
background: rgba(24, 24, 27, 0.8);
padding: 8px 12px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border);
cursor: grab;
user-select: none;
}
.insight-header .title { font-weight: 500; flex-grow: 1; }
.insight-header .close-btn { cursor: pointer; color: var(--text-muted); }
.insight-header .close-btn:hover { color: #F87171; }
.window-content {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
}
.btn {
background: var(--bg-hover);
border: 1px solid var(--border);
color: var(--text-main);
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-family: var(--font-sans);
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover { background: var(--border); }
input.dark-input {
background: var(--bg-base);
border: 1px solid var(--border);
color: var(--text-main);
padding: 6px 8px;
border-radius: 4px;
outline: none;
width: 100%;
font-family: var(--font-sans);
}
input.dark-input:focus { border-color: var(--accent); }
/* Two Way Data Binding Inputs */
.live-input {
background: transparent;
border: 1px solid transparent;
color: var(--accent);
font-family: var(--font-mono);
font-size: 12px;
width: 60px;
outline: none;
text-align: right;
border-radius: 3px;
}
.live-input:focus {
border-color: var(--accent);
background: var(--bg-base);
}
.cmd-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px);
display: flex; justify-content: center; padding-top: 15vh; z-index: 10000;
}
.cmd-palette {
width: 600px; background: var(--bg-surface);
border: 1px solid var(--border); border-radius: 8px;
display: flex; flex-direction: column;
}
.cmd-input {
width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--border);
color: var(--text-main); font-size: 16px; padding: 16px; outline: none;
}
.cmd-item {
padding: 12px 16px; display: flex; align-items: center; cursor: pointer; color: var(--text-muted);
}
.cmd-item.selected { background: var(--bg-hover); color: var(--text-main); }
.sidebar-layout { display: flex; height: 100%; width: 100%; }
.sidebar { width: 150px; border-right: 1px solid var(--border); background: rgba(24, 24, 27, 0.5); display: flex; flex-direction: column; }
.sidebar-item { padding: 10px 12px; cursor: pointer; color: var(--text-muted); display: flex; align-items: center; gap: 8px; border-left: 2px solid transparent; }
.sidebar-item:hover, .sidebar-item.active { background: var(--bg-hover); color: var(--text-main); }
.sidebar-item.active { border-left-color: var(--accent); }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
`;
this.shadowRoot.appendChild(style);
}
loadDependencies() {
const script = document.createElement('script');
script.src = 'https://unpkg.com/lucide@latest';
script.onload = () => {
this.iconsLoaded = true;
this.refreshIcons(this.shadowRoot);
};
document.head.appendChild(script);
}
refreshIcons(root) {
if (this.iconsLoaded && window.lucide) {
window.lucide.createIcons({
root: root,
attrs: { class: "lucide-icon", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }
});
}
}
createWindow(id, title, iconName) {
return new UIWindow(this, id, title, iconName);
}
showToast(message) {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed; bottom: 24px; right: 24px;
background: var(--bg-surface); border: 1px solid var(--border);
color: var(--text-main); padding: 12px 24px; border-radius: 8px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
font-weight: 500; z-index: 10000; transition: all 0.3s;
`;
toast.innerHTML = `<div style="display: flex; align-items: center; gap: 8px;"><i data-lucide="info" style="width: 16px; height: 16px; color: var(--accent);"></i> ${message}</div>`;
this.shadowRoot.appendChild(toast);
this.refreshIcons(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
}
/**
* =========================================================================
* 1. DETECTOR MODULE (Intelligent Hooks + Safe Mode Fallback)
* =========================================================================
*/
class ThreeDetector extends Module {
init() {
this.scenes = new Set();
this.cameras = new Set();
this.renderers = new Set();
this.textures = new Set();
this.materials = new Set();
this.geometries = new Set();
this.lastCamera = null;
this.lastScene = null;
this.lastRenderer = null;
this.setupHooks();
}
setupHooks() {
let _THREE = window.THREE;
Object.defineProperty(window, 'THREE', {
get: () => _THREE,
set: (val) => {
_THREE = val;
if (val) this.hookThree(val);
},
configurable: true
});
if (_THREE) this.hookThree(_THREE);
}
hookThree(THREE) {
if (THREE._insightHooked) return;
try {
// High-performance Proxy hooks
const proxySubclasses = (baseName, callback) => {
Object.keys(THREE).forEach(key => {
if (key.includes(baseName) || key === baseName) {
if (typeof THREE[key] === 'function') {
const Orig = THREE[key];
if (!Orig.__insightHooked) {
const hooked = new Proxy(Orig, {
construct(target, args) {
const obj = new target(...args);
callback(obj);
return obj;
}
});
hooked.__insightHooked = true;
THREE[key] = hooked;
}
}
}
});
};
proxySubclasses('Scene', (o) => { this.scenes.add(o); this.emit('asset-added', { type: 'scene', obj: o }); });
proxySubclasses('WebGLRenderer', (o) => { this.renderers.add(o); this.emit('asset-added', { type: 'renderer', obj: o }); });
proxySubclasses('Texture', (o) => { this.textures.add(o); this.emit('asset-added', { type: 'texture', obj: o }); });
proxySubclasses('Material', (o) => { this.materials.add(o); this.emit('asset-added', { type: 'material', obj: o }); });
proxySubclasses('Geometry', (o) => { this.geometries.add(o); this.emit('asset-added', { type: 'geometry', obj: o }); });
proxySubclasses('Camera', (o) => { this.cameras.add(o); this.emit('asset-added', { type: 'camera', obj: o }); });
// Hook the renderer's loop to catch pre-existing scenes and active cameras
if (THREE.WebGLRenderer) {
const origRender = THREE.WebGLRenderer.prototype.render;
const self = this;
THREE.WebGLRenderer.prototype.render = function(scene, camera) {
self.lastRenderer = this;
self.lastScene = scene;
self.lastCamera = camera;
if (!self.renderers.has(this)) { self.renderers.add(this); self.emit('asset-added', { type: 'renderer', obj: this }); }
if (scene && !self.scenes.has(scene)) { self.scenes.add(scene); self.emit('asset-added', { type: 'scene', obj: scene }); }
return origRender.apply(this, arguments);
};
}
THREE._insightHooked = true;
console.log('[Insight] Intelligent WebGL hooking initialized.');
} catch (err) {
// Safe Mode Fallback for heavily obfuscated / frozen objects
console.warn('[Insight] Proxy Hook failed. Falling back to Safe Mode (Interval Scanning).', err);
setInterval(() => this.scanGlobal(), 2000);
}
}
scanGlobal() {
const traverse = (obj) => {
if (!obj || typeof obj !== 'object') return;
if (obj.isScene) { this.scenes.add(obj); this.lastScene = obj; }
if (obj.isCamera) { this.cameras.add(obj); this.lastCamera = obj; }
if (obj.isTexture) this.textures.add(obj);
if (obj.isMaterial) this.materials.add(obj);
if (obj.isBufferGeometry || obj.isGeometry) this.geometries.add(obj);
if (obj.children) obj.children.forEach(traverse);
};
this.scenes.forEach(traverse);
}
}
/**
* =========================================================================
* 2. RAYCASTER PICKER TOOL (Interactive Click-to-Select)
* =========================================================================
*/
class PickerTool extends Module {
init() {
this.active = false;
this.raycaster = new window.THREE.Raycaster();
}
initUI() {
this.insight.commands.registerCommand('Toggle Raycast Picker', 'crosshair', () => this.toggle());
document.addEventListener('click', (e) => this.onClick(e), true);
}
toggle() {
this.active = !this.active;
this.insight.ui.showToast(`Picker Tool ${this.active ? 'Activated (Click an object)' : 'Deactivated'}`);
if (this.active) {
document.body.style.cursor = 'crosshair';
} else {
document.body.style.cursor = 'default';
}
}
onClick(e) {
if (!this.active || !this.insight.modules.detector.lastCamera || !this.insight.modules.detector.lastScene) return;
e.preventDefault();
e.stopPropagation();
const renderer = this.insight.modules.detector.lastRenderer;
const canvas = renderer ? renderer.domElement : document.body;
const rect = canvas.getBoundingClientRect();
const mouse = new window.THREE.Vector2();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(mouse, this.insight.modules.detector.lastCamera);
const intersects = this.raycaster.intersectObjects(this.insight.modules.detector.lastScene.children, true);
if (intersects.length > 0) {
this.insight.emit('inspect-object', intersects[0].object);
this.insight.ui.showToast(`Selected: ${intersects[0].object.name || intersects[0].object.type}`);
}
this.toggle(); // Auto-disable after picking
}
}
/**
* =========================================================================
* 3. ASSET EXPLORER (Snapshots, Gallery Views, Downloads)
* =========================================================================
*/
class AssetExplorer extends Module {
initUI() {
this.insight.commands.registerCommand('Asset Explorer', 'package', () => this.openWindow());
}
openWindow() {
const win = this.insight.ui.createWindow('asset_explorer', 'Asset Explorer', 'package');
if (win.el.style.width === 'auto') {
win.el.style.width = '750px';
win.el.style.height = '500px';
}
win.content.innerHTML = `
<div class="sidebar-layout">
<div class="sidebar">
<div class="sidebar-item active" data-tab="textures"><i data-lucide="image"></i> Textures</div>
<div class="sidebar-item" data-tab="materials"><i data-lucide="layers"></i> Materials</div>
<div class="sidebar-item" data-tab="geometries"><i data-lucide="box"></i> Geometry</div>
<div style="margin-top: auto; padding: 10px; border-top: 1px solid var(--border);">
<button id="btn-snapshot" class="btn" style="width: 100%; justify-content: center; margin-bottom: 6px;">
<i data-lucide="camera"></i> Snapshot
</button>
<label style="display: flex; align-items: center; gap: 6px; font-size: 11px;">
<input type="checkbox" id="chk-delta" disabled> Show Delta (Leaks)
</label>
</div>
</div>
<div class="main-content">
<div style="padding: 8px; border-bottom: 1px solid var(--border); display: flex; gap: 8px;">
<input type="text" class="dark-input" id="asset-search" placeholder="Search assets..." />
</div>
<div id="asset-list" style="flex: 1; overflow-y: auto; padding: 12px; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); align-content: start;">
</div>
</div>
</div>
`;
let currentTab = 'textures';
let searchQuery = '';
let snapshot = null;
let showDelta = false;
const listEl = win.content.querySelector('#asset-list');
const renderItems = () => {
listEl.innerHTML = '';
const detector = this.insight.modules.detector;
let items = [];
if (currentTab === 'textures') items = Array.from(detector.textures);
if (currentTab === 'materials') items = Array.from(detector.materials);
if (currentTab === 'geometries') items = Array.from(detector.geometries);
// Filter by Snapshot Delta
if (showDelta && snapshot) {
items = items.filter(i => !snapshot.has(i.uuid));
}
items.forEach(item => {
const name = item.name || item.type || 'Unnamed';
if (searchQuery && !name.toLowerCase().includes(searchQuery.toLowerCase())) return;
const card = document.createElement('div');
card.style.cssText = 'background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px; display: flex; flex-direction: column; gap: 6px;';
let meta = '';
let preview = '';
if (currentTab === 'textures') {
meta = `<div style="font-size: 10px;">${item.image ? item.image.width+'x'+item.image.height : 'No Image'} | ${item.format}</div>`;
// Gallery View implementation
if (item.image && item.image.src) {
preview = `<div style="height: 100px; background: #000 url(${item.image.src}) center/contain no-repeat; border-radius: 4px; margin-bottom: 6px;"></div>`;
meta += `<button class="btn dl-btn" data-url="${item.image.src}" style="width: 100%; margin-top: 6px; padding: 4px;">Download Map</button>`;
} else if (item.image && item.image instanceof HTMLCanvasElement) {
preview = `<div style="height: 100px; background: #000 url(${item.image.toDataURL()}) center/contain no-repeat; border-radius: 4px; margin-bottom: 6px;"></div>`;
meta += `<button class="btn dl-btn" data-url="${item.image.toDataURL()}" style="width: 100%; margin-top: 6px; padding: 4px;">Download Map</button>`;
}
} else if (currentTab === 'materials') {
meta = `<div style="font-size: 10px;">Type: ${item.type}<br>Wireframe: ${item.wireframe}</div>`;
} else if (currentTab === 'geometries') {
const verts = item.attributes?.position?.count || 0;
const tris = item.index ? item.index.count / 3 : verts / 3;
meta = `<div style="font-size: 10px;">Verts: ${verts.toLocaleString()}<br>Tris: ${Math.floor(tris).toLocaleString()}</div>`;
meta += `<button class="btn exp-btn" style="width: 100%; margin-top: 6px; padding: 4px;">Export OBJ</button>`;
}
card.innerHTML = `
${preview}
<div style="font-weight: 600; font-size: 12px; color: var(--accent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${name}">${name}</div>
<div style="color: var(--text-muted); font-family: var(--font-mono);">${meta}</div>
`;
// Actions
const dlBtn = card.querySelector('.dl-btn');
if(dlBtn) {
dlBtn.onclick = () => {
const a = document.createElement('a');
a.href = dlBtn.getAttribute('data-url');
a.download = `${name}.png`;
a.click();
};
}
const expBtn = card.querySelector('.exp-btn');
if(expBtn) {
expBtn.onclick = () => {
const mesh = new window.THREE.Mesh(item, new window.THREE.MeshBasicMaterial());
mesh.name = name;
exportToOBJ(mesh);
};
}
listEl.appendChild(card);
});
};
// Memory Snapshot Logic
const snapBtn = win.content.querySelector('#btn-snapshot');
const chkDelta = win.content.querySelector('#chk-delta');
snapBtn.onclick = () => {
snapshot = new Set([
...Array.from(this.insight.modules.detector.textures).map(t => t.uuid),
...Array.from(this.insight.modules.detector.materials).map(m => m.uuid),
...Array.from(this.insight.modules.detector.geometries).map(g => g.uuid)
]);
chkDelta.disabled = false;
chkDelta.checked = true;
showDelta = true;
this.insight.ui.showToast(`Memory Snapshot Taken. Identifying Deltas.`);
renderItems();
};
chkDelta.onchange = (e) => {
showDelta = e.target.checked;
renderItems();
};
win.content.querySelectorAll('.sidebar-item').forEach(el => {
el.addEventListener('click', (e) => {
win.content.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
e.currentTarget.classList.add('active');
currentTab = e.currentTarget.getAttribute('data-tab');
renderItems();
});
});
win.content.querySelector('#asset-search').addEventListener('input', (e) => {
searchQuery = e.target.value;
renderItems();
});
const cleanup = this.insight.modules.detector.on('asset-added', () => {
if(!showDelta) renderItems();
});
win.onClose(cleanup);
renderItems();
this.insight.ui.refreshIcons(win.content);
}
}
/**
* =========================================================================
* 4. ENTITY INSPECTOR (Live Editing + Two-Way Binding + Shader Viewer)
* =========================================================================
*/
class EntityInspector extends Module {
initUI() {
this.insight.commands.registerCommand('Entity Inspector', 'sliders', () => this.openWindow());
this.insight.on('inspect-object', (obj) => {
this.activeObject = obj;
if (!this.window) this.window = this.openWindow();
this.rebuildInspectorDOM();
});
}
openWindow() {
const win = this.insight.ui.createWindow('entity_inspector', 'Inspector', 'sliders');
if (win.el.style.width === 'auto') {
win.el.style.width = '320px';
win.el.style.height = '600px';
}
this.window = win;
this.liveRefs = {};
let rAF;
const loop = () => {
if (this.activeObject && this.window) this.updateLiveValues();
rAF = requestAnimationFrame(loop);
};
rAF = requestAnimationFrame(loop);
win.onClose(() => {
this.window = null;
cancelAnimationFrame(rAF);
});
this.rebuildInspectorDOM();
return win;
}
rebuildInspectorDOM() {
if (!this.window) return;
const content = this.window.content;
const obj = this.activeObject;
this.liveRefs = {};
if (!obj) {
content.innerHTML = '<div style="padding: 12px; color: var(--text-muted); text-align: center;">Select an object.</div>';
return;
}
// Input UI Generators
const createNum = (key) => {
this.liveRefs[key] = Math.random().toString(36).substr(2, 9);
return `<input id="${this.liveRefs[key]}" class="live-input" type="number" step="0.1">`;
};
const createBool = (key) => {
this.liveRefs[key] = Math.random().toString(36).substr(2, 9);
return `<input id="${this.liveRefs[key]}" type="checkbox">`;
};
const createColor = (key) => {
this.liveRefs[key] = Math.random().toString(36).substr(2, 9);
return `<input id="${this.liveRefs[key]}" type="color" style="background:transparent;border:none;cursor:pointer;width:24px;height:24px;padding:0;">`;
};
const section = (title, inner) => `
<div style="border-bottom: 1px solid var(--border); padding: 12px;">
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; margin-bottom: 8px;">${title}</div>
${inner}
</div>
`;
const row = (lbl, val) => `
<div style="display: flex; justify-content: space-between; align-items:center; margin-bottom: 4px; font-size: 12px;">
<span style="color: var(--text-muted);">${lbl}</span>
<span style="font-family: var(--font-mono); color: var(--text-main);">${val}</span>
</div>
`;
let html = section('Information',
row('Name', `<span style="color: var(--accent);">${obj.name || 'N/A'}</span>`) +
row('Type', obj.type) +
row('UUID', `<span style="font-size: 9px;">${obj.uuid}</span>`)
);
if (obj.position) {
html += section('Transform',
row('Position', `${createNum('px')} ${createNum('py')} ${createNum('pz')}`) +
row('Rotation', `${createNum('rx')} ${createNum('ry')} ${createNum('rz')}`) +
row('Scale', `${createNum('sx')} ${createNum('sy')} ${createNum('sz')}`)
);
}
if (obj.material) {
const mat = obj.material;
html += section('Material',
row('Type', mat.type) +
(mat.color ? row('Color (Live)', createColor('mCol')) : '') +
row('Wireframe', createBool('mWire')) +
(mat.isShaderMaterial || mat.vertexShader ? `<button id="btn-shader" class="btn" style="width:100%; margin-top:8px;">Inspect Shader Source</button>` : '')
);
}
html += section('Properties',
row('Visible', createBool('visible')) +
`<button id="btn-export" class="btn" style="width:100%; margin-top:8px;">Export as OBJ</button>`
);
content.innerHTML = html;
// Bind Event Listeners for Live Editing (Two-Way Data Binding)
const bindEl = (key, event, handler) => {
const el = content.querySelector('#' + this.liveRefs[key]);
if (el) {
el.addEventListener(event, handler);
this.liveRefs[key] = el; // Store DOM element
}
};
if (obj.position) {
bindEl('px', 'input', e => obj.position.x = parseFloat(e.target.value));
bindEl('py', 'input', e => obj.position.y = parseFloat(e.target.value));
bindEl('pz', 'input', e => obj.position.z = parseFloat(e.target.value));
bindEl('rx', 'input', e => obj.rotation.x = parseFloat(e.target.value));
bindEl('ry', 'input', e => obj.rotation.y = parseFloat(e.target.value));
bindEl('rz', 'input', e => obj.rotation.z = parseFloat(e.target.value));
bindEl('sx', 'input', e => obj.scale.x = parseFloat(e.target.value));
bindEl('sy', 'input', e => obj.scale.y = parseFloat(e.target.value));
bindEl('sz', 'input', e => obj.scale.z = parseFloat(e.target.value));
}
bindEl('visible', 'change', e => obj.visible = e.target.checked);
if (obj.material) {
bindEl('mWire', 'change', e => { obj.material.wireframe = e.target.checked; obj.material.needsUpdate = true; });
bindEl('mCol', 'input', e => { if(obj.material.color) obj.material.color.set(e.target.value); });
// Shader Source Viewer
const btnShader = content.querySelector('#btn-shader');
if (btnShader) {
btnShader.onclick = () => {
const win = this.insight.ui.createWindow('shader_view', 'Shader Source', 'code');
win.el.style.width = '700px'; win.el.style.height = '500px';
win.content.innerHTML = `
<div style="display:flex; height:100%;">
<textarea style="flex:1; background:#111; color:#10B981; font-family:var(--font-mono); font-size:11px; padding:10px; border:none; outline:none; resize:none;" readonly>// VERTEX SHADER\n${obj.material.vertexShader}</textarea>
<textarea style="flex:1; background:#111; color:#3B82F6; font-family:var(--font-mono); font-size:11px; padding:10px; border:none; outline:none; resize:none; border-left:1px solid var(--border);" readonly>// FRAGMENT SHADER\n${obj.material.fragmentShader}</textarea>
</div>`;
};
}
}
const btnExport = content.querySelector('#btn-export');
if (btnExport) btnExport.onclick = () => exportToOBJ(obj);
}
updateLiveValues() {
const obj = this.activeObject;
const refs = this.liveRefs;
const updateNum = (el, val) => { if (el && document.activeElement !== el) el.value = typeof val === 'number' ? val.toFixed(3) : val; };
const updateBool = (el, val) => { if (el && document.activeElement !== el) el.checked = val; };
const updateCol = (el, val) => { if (el && document.activeElement !== el && val) el.value = '#' + val.getHexString(); };
if (obj.position) {
updateNum(refs.px, obj.position.x); updateNum(refs.py, obj.position.y); updateNum(refs.pz, obj.position.z);
updateNum(refs.rx, obj.rotation.x); updateNum(refs.ry, obj.rotation.y); updateNum(refs.rz, obj.rotation.z);
updateNum(refs.sx, obj.scale.x); updateNum(refs.sy, obj.scale.y); updateNum(refs.sz, obj.scale.z);
}
updateBool(refs.visible, obj.visible);
if (obj.material) {
updateBool(refs.mWire, obj.material.wireframe);
updateCol(refs.mCol, obj.material.color);
}
}
}
/**
* =========================================================================
* 5. SCENE EXPLORER (With Heavy Bottleneck Highlighting)
* =========================================================================
*/
class SceneExplorer extends Module {
initUI() {
this.insight.commands.registerCommand('Scene Hierarchy', 'layers', () => this.openWindow());
}
openWindow() {
const win = this.insight.ui.createWindow('scene_explorer', 'Hierarchy', 'layers');
if (win.el.style.width === 'auto') {
win.el.style.width = '350px';
win.el.style.height = '500px';
}
const toolbar = document.createElement('div');
toolbar.style.padding = '8px';
toolbar.style.borderBottom = '1px solid var(--border)';
const search = document.createElement('input');
search.className = 'dark-input';
search.placeholder = 'Filter nodes...';
toolbar.appendChild(search);
win.content.appendChild(toolbar);
const treeContainer = document.createElement('div');
treeContainer.style.flex = '1';
treeContainer.style.overflow = 'auto';
treeContainer.style.padding = '8px';
win.content.appendChild(treeContainer);
search.addEventListener('input', () => this.renderTree(treeContainer, search.value.toLowerCase()));
const cleanup = this.insight.modules.detector.on('asset-added', (d) => {
if (d.type === 'scene') this.renderTree(treeContainer, search.value.toLowerCase());
});
win.onClose(cleanup);
this.renderTree(treeContainer, '');
}
renderTree(container, filterText) {
container.innerHTML = '';
const scenes = Array.from(this.insight.modules.detector.scenes);
if (scenes.length === 0) return;
const buildNode = (object) => {
const name = (object.name || object.type || 'Object3D').toLowerCase();
let childrenNodes = [];
let hasMatchingDescendant = false;
if (object.children && object.children.length > 0) {
object.children.forEach(child => {
const childResult = buildNode(child);
if (childResult) { childrenNodes.push(childResult.el); hasMatchingDescendant = true; }
});
}
if (filterText !== '' && !name.includes(filterText) && !hasMatchingDescendant) return null;
const node = document.createElement('div');
const row = document.createElement('div');
row.style.cssText = 'display: flex; align-items: center; padding: 4px; cursor: pointer; border-radius: 4px;';
row.onmouseenter = () => row.style.background = 'var(--bg-hover)';
row.onmouseleave = () => row.style.background = 'transparent';
// Performance Bottleneck logic
let isHeavy = false;
if (object.geometry) {
const verts = object.geometry.attributes?.position?.count || 0;
if (verts > 100000) isHeavy = true;
}
const hasChildren = childrenNodes.length > 0;
const chevronHtml = hasChildren ? `<i data-lucide="chevron-down" style="width: 14px; margin-right: 4px; color: var(--text-muted);"></i>` : `<span style="width: 18px; display: inline-block;"></span>`;
const iconHtml = `<i data-lucide="${object.type === 'Mesh' ? 'box' : 'cuboid'}" style="width: 14px; margin-right: 6px; color: ${isHeavy ? '#EF4444' : 'var(--text-muted)'};"></i>`;
row.innerHTML = `
${chevronHtml}${iconHtml}
<span style="font-size: 13px; color: ${isHeavy ? '#EF4444' : (object.visible ? 'var(--text-main)' : 'var(--text-muted)')};">${object.name || object.type || 'Object3D'}</span>
${isHeavy ? '<span style="font-size:9px;color:#EF4444;margin-left:auto;font-weight:bold;">HEAVY</span>' : ''}
`;
node.appendChild(row);
if (hasChildren) {
const childrenContainer = document.createElement('div');
childrenContainer.style.cssText = 'padding-left: 14px; border-left: 1px solid var(--border); margin-left: 11px;';
childrenNodes.forEach(childEl => childrenContainer.appendChild(childEl));
node.appendChild(childrenContainer);
row.querySelector('i[data-lucide="chevron-down"]').onclick = (e) => {
e.stopPropagation();
const isHidden = childrenContainer.style.display === 'none';
childrenContainer.style.display = isHidden ? 'block' : 'none';
e.target.setAttribute('data-lucide', isHidden ? 'chevron-down' : 'chevron-right');
this.insight.ui.refreshIcons(row);
};
}
row.addEventListener('click', () => this.insight.emit('inspect-object', object));
return { el: node };
};
scenes.forEach(scene => {
const res = buildNode(scene);
if (res) container.appendChild(res.el);
});
this.insight.ui.refreshIcons(container);
}
}
/**
* =========================================================================
* 6. RUNTIME OBJECT EXPLORER (The Graph View)
* =========================================================================
*/
class RuntimeObjectExplorer extends Module {
initUI() {
this.insight.commands.registerCommand('Runtime Object Graph', 'share-2', () => this.openWindow());
}
openWindow() {
const win = this.insight.ui.createWindow('runtime_graph', 'Runtime Graph', 'share-2');
if (win.el.style.width === 'auto') {
win.el.style.width = '800px';
win.el.style.height = '600px';
}
const toolbar = document.createElement('div');
toolbar.style.padding = '8px';
toolbar.style.borderBottom = '1px solid var(--border)';
toolbar.style.display = 'flex';
toolbar.style.gap = '8px';
const searchInput = document.createElement('input');
searchInput.className = 'dark-input';
searchInput.placeholder = 'Search nodes...';
searchInput.style.width = '250px';
const rebuildBtn = document.createElement('button');
rebuildBtn.className = 'btn';
rebuildBtn.innerHTML = '<i data-lucide="refresh-cw"></i> Rebuild Graph';
toolbar.appendChild(searchInput);
toolbar.appendChild(rebuildBtn);
win.content.appendChild(toolbar);
const canvasContainer = document.createElement('div');
canvasContainer.style.flex = '1';
canvasContainer.style.overflow = 'hidden';
canvasContainer.style.position = 'relative';
canvasContainer.style.background = '#111';
win.content.appendChild(canvasContainer);
const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvasContainer.appendChild(canvas);
let ctx = canvas.getContext('2d');
let nodes = [];
let edges = [];
let transform = { x: 0, y: 0, scale: 1 };
let isDragging = false;
let dragNode = null;
let lastMouse = { x: 0, y: 0 };
const resizeCanvas = () => {
canvas.width = canvasContainer.clientWidth;
canvas.height = canvasContainer.clientHeight;
if(transform.x === 0) {
transform.x = canvas.width / 2;
transform.y = canvas.height / 2;
}
};
new ResizeObserver(resizeCanvas).observe(canvasContainer);
resizeCanvas();
const buildGraph = () => {
nodes = [];
edges = [];
const detector = this.insight.modules.detector;
let idCounter = 0;
const objMap = new Map();
const addNode = (obj, label, type) => {
if (objMap.has(obj)) return objMap.get(obj);
const node = {
id: idCounter++, obj, label, type,
x: Math.random() * 800 - 400,
y: Math.random() * 600 - 300,
vx: 0, vy: 0, highlighted: false
};
nodes.push(node);
objMap.set(obj, node);
return node;
};
const addEdge = (from, to, label) => {
edges.push({ from, to, label });
};
detector.renderers.forEach((r, i) => {
const rNode = addNode(r, `WebGLRenderer ${i}`, 'renderer');
detector.scenes.forEach(s => {
const sNode = addNode(s, s.name || `Scene ${s.uuid.substr(0,4)}`, 'scene');
addEdge(rNode, sNode, 'renders');
s.children.forEach(c => {
const cNode = addNode(c, c.name || c.type, 'object');
addEdge(sNode, cNode, 'child');
});
});
});
};
buildGraph();
// Spring Physics Interaction
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left - transform.x) / transform.scale;
const my = (e.clientY - rect.top - transform.y) / transform.scale;
dragNode = nodes.find(n => Math.abs(n.x - mx) < 50 && Math.abs(n.y - my) < 20);
isDragging = true;
lastMouse = { x: e.clientX, y: e.clientY };
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - lastMouse.x;
const dy = e.clientY - lastMouse.y;
if (dragNode) {
dragNode.x += dx / transform.scale;
dragNode.y += dy / transform.scale;
dragNode.vx = 0; dragNode.vy = 0;
} else {
transform.x += dx;
transform.y += dy;
}
lastMouse = { x: e.clientX, y: e.clientY };
});
canvas.addEventListener('mouseup', () => { isDragging = false; dragNode = null; });
canvas.addEventListener('mouseleave', () => { isDragging = false; dragNode = null; });
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const zoom = Math.exp(-e.deltaY * 0.001);
transform.scale *= zoom;
});
searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase();
nodes.forEach(n => { n.highlighted = query && n.label.toLowerCase().includes(query); });
});
rebuildBtn.addEventListener('click', buildGraph);
let rAF;
const loop = () => {
if (!ctx) return;
// Layout Physics
if (!dragNode) {
for(let i=0; i<nodes.length; i++) {
for(let j=i+1; j<nodes.length; j++) {
const n1 = nodes[i], n2 = nodes[j];
let dx = n1.x - n2.x, dy = n1.y - n2.y;
let distSq = dx*dx + dy*dy || 1;
if(distSq < 40000) {
let f = 1000 / distSq;
n1.vx += dx*f; n1.vy += dy*f;
n2.vx -= dx*f; n2.vy -= dy*f;
}
}
}
edges.forEach(e => {
let dx = e.to.x - e.from.x, dy = e.to.y - e.from.y;
let dist = Math.sqrt(dx*dx + dy*dy) || 1;
let f = (dist - 100) * 0.005;
e.from.vx += (dx/dist)*f; e.from.vy += (dy/dist)*f;
e.to.vx -= (dx/dist)*f; e.to.vy -= (dy/dist)*f;
});
nodes.forEach(n => {
n.x += n.vx; n.y += n.vy;
n.vx *= 0.85; n.vy *= 0.85;
});
}
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(transform.x, transform.y);
ctx.scale(transform.scale, transform.scale);
// Draw Edges
ctx.lineWidth = 1;
edges.forEach(e => {
ctx.beginPath();
ctx.moveTo(e.from.x, e.from.y);
ctx.lineTo(e.to.x, e.to.y);
ctx.strokeStyle = 'rgba(100, 100, 100, 0.5)';
ctx.stroke();
});
// Draw Nodes
ctx.font = '12px Inter';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
nodes.forEach(n => {
ctx.fillStyle = n.highlighted ? '#F59E0B' : (n.type === 'scene' ? '#3B82F6' : '#27272A');
ctx.strokeStyle = n.highlighted ? '#FFF' : '#3F3F46';
ctx.lineWidth = 2;
const w = Math.max(100, ctx.measureText(n.label).width + 20);
ctx.fillRect(n.x - w/2, n.y - 15, w, 30);
ctx.strokeRect(n.x - w/2, n.y - 15, w, 30);
ctx.fillStyle = n.highlighted ? '#000' : '#FFF';
ctx.fillText(n.label, n.x, n.y);
});
ctx.restore();
rAF = requestAnimationFrame(loop);
};
rAF = requestAnimationFrame(loop);
win.onClose(() => {
cancelAnimationFrame(rAF);
ctx = null;
});
this.insight.ui.refreshIcons(toolbar);
}
}
/**
* =========================================================================
* 7. NETWORK ANALYZER
* =========================================================================
*/
class NetworkAnalyzer extends Module {
init() {
this.requests = [];
this.hookFetch();
this.hookXHR();
}
initUI() {
this.insight.commands.registerCommand('Network Analyzer', 'globe', () => this.openWindow());
}
hookFetch() {
const origFetch = window.fetch;
window.fetch = async (...args) => {
this.addReq(args[0], args[1]?.method || 'GET', 'fetch');
return origFetch.apply(window, args);
};
}
hookXHR() {
const origOpen = XMLHttpRequest.prototype.open;
const self = this;
XMLHttpRequest.prototype.open = function(method, url) {
self.addReq(url, method, 'xhr');
return origOpen.apply(this, arguments);
};
}
addReq(url, method, type) {
const req = { url, method, type, time: new Date().toLocaleTimeString() };
this.requests.push(req);
if (this.requests.length > 200) this.requests.shift(); // Safety limit
this.emit('new-request', req);
}
openWindow() {
const win = this.insight.ui.createWindow('network_analyzer', 'Network', 'globe');
if(win.el.style.width === 'auto') { win.el.style.width = '600px'; win.el.style.height = '400px'; }
const tableContainer = document.createElement('div');
tableContainer.style.cssText = 'width: 100%; height: 100%; overflow: auto;';
win.content.appendChild(tableContainer);
const render = () => {
let html = `<table style="width: 100%; text-align: left; border-collapse: collapse;">
<tr style="border-bottom: 1px solid var(--border); color: var(--text-muted); background: var(--bg-base); position: sticky; top: 0;">
<th style="padding: 8px;">Time</th><th style="padding: 8px;">Method</th><th style="padding: 8px;">Type</th><th style="padding: 8px;">URL</th>
</tr>`;
this.requests.slice().reverse().forEach(r => {
html += `<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 8px; font-family: var(--font-mono); font-size: 11px;">${r.time}</td>
<td style="padding: 8px; color: var(--accent); font-weight: 600;">${r.method}</td>
<td style="padding: 8px; color: var(--text-muted);">${r.type}</td>
<td style="padding: 8px; max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${r.url}</td>
</tr>`;
});
tableContainer.innerHTML = html + '</table>';
};
render();
const cleanup = this.on('new-request', render);
win.onClose(cleanup);
}
}
/**
* =========================================================================
* 8. FUNCTION TRACER (With Helper UI)
* =========================================================================
*/
class FunctionTracer extends Module {
initUI() {
this.insight.commands.registerCommand('Function Hooks', 'activity', () => this.openWindow());
}
trace(obj, methodName) {
if(!obj || !obj[methodName]) return;
const orig = obj[methodName];
if(orig.__isHooked) return;
const self = this;
obj[methodName] = function(...args) {
const start = performance.now();
const res = orig.apply(this, args);
self.emit('trace', { method: methodName, duration: performance.now() - start });
return res;
};
obj[methodName].__isHooked = true;
this.insight.ui.showToast(`Hook attached to ${methodName}`);
}
openWindow() {
const win = this.insight.ui.createWindow('func_tracer', 'Function Hooks', 'activity');
if (win.el.style.width === 'auto') win.el.style.width = '400px';
const hookBtn = (label, target, method) => {
const btn = document.createElement('button');
btn.className = 'btn';
btn.style.width = '100%';
btn.style.marginBottom = '8px';
btn.innerHTML = `<i data-lucide="zap"></i> Hook ${label}`;
btn.onclick = () => { if(window.THREE && window.THREE[target]) this.trace(window.THREE[target].prototype, method); };
return btn;
};
const container = document.createElement('div');
container.style.padding = '16px';
container.appendChild(hookBtn('Renderer.render', 'WebGLRenderer', 'render'));
container.appendChild(hookBtn('Raycaster.intersectObject', 'Raycaster', 'intersectObject'));
container.appendChild(hookBtn('Object3D.updateMatrixWorld', 'Object3D', 'updateMatrixWorld'));
win.content.appendChild(container);
this.insight.ui.refreshIcons(win.content);
}
}
/**
* =========================================================================
* 9. PERFORMANCE MONITOR
* =========================================================================
*/
class PerformanceMonitor extends Module {
init() {
this.fps = 0;
this.drawCalls = 0;
this.triangles = 0;
this.hookWebGL();
this.startLoop();
}
initUI() {
this.insight.commands.registerCommand('Performance Dashboard', 'cpu', () => this.openWindow());
}
hookWebGL() {
const self = this;
const origDrawElements = WebGLRenderingContext.prototype.drawElements;
WebGLRenderingContext.prototype.drawElements = function(mode, count, type, offset) {
self.drawCalls++;
if (mode === this.TRIANGLES) self.triangles += count / 3;
return origDrawElements.apply(this, arguments);
};
if (window.WebGL2RenderingContext) {
const origDrawElements2 = WebGL2RenderingContext.prototype.drawElements;
WebGL2RenderingContext.prototype.drawElements = function(mode, count, type, offset) {
self.drawCalls++;
if (mode === this.TRIANGLES) self.triangles += count / 3;
return origDrawElements2.apply(this, arguments);
};
}
}
startLoop() {
let frames = 0, lastTime = performance.now();
const loop = () => {
frames++;
const now = performance.now();
if (now >= lastTime + 1000) {
this.fps = (frames * 1000) / (now - lastTime);
this.emit('stats', { fps: this.fps, drawCalls: this.drawCalls, triangles: this.triangles });
frames = 0; lastTime = now; this.drawCalls = 0; this.triangles = 0;
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
openWindow() {
const win = this.insight.ui.createWindow('perf_mon', 'Performance', 'cpu');
const updateUI = (stats) => {
win.content.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 12px; padding: 12px;">
<div style="display: flex; justify-content: space-between;"><span style="color: var(--text-muted);">FPS</span><span style="font-family: var(--font-mono); font-size: 18px; color: ${stats.fps > 50 ? '#34D399' : '#FBBF24'};">${Math.round(stats.fps)}</span></div>
<div style="display: flex; justify-content: space-between;"><span style="color: var(--text-muted);">Draw Calls / s</span><span style="font-family: var(--font-mono); color: var(--accent);">${stats.drawCalls}</span></div>
<div style="display: flex; justify-content: space-between;"><span style="color: var(--text-muted);">Triangles / s</span><span style="font-family: var(--font-mono); color: var(--accent);">${Math.round(stats.triangles).toLocaleString()}</span></div>
</div>
`;
};
updateUI({ fps: this.fps, drawCalls: this.drawCalls, triangles: this.triangles });
win.onClose(this.on('stats', updateUI));
}
}
/**
* =========================================================================
* 10. DEVELOPER CONSOLE
* =========================================================================
*/
class DeveloperConsole extends Module {
initUI() {
this.insight.commands.registerCommand('Developer Console', 'terminal', () => this.openWindow());
}
openWindow() {
const win = this.insight.ui.createWindow('dev_console', 'Console', 'terminal');
if (win.el.style.width === 'auto') { win.el.style.width = '600px'; win.el.style.height = '400px'; }
win.content.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<div class="console-output" style="flex: 1; overflow-y: auto; padding: 12px; font-family: var(--font-mono); font-size: 12px; background: #000; margin: 8px;">
<div style="color: var(--text-muted);">// Insight Platform API available as 'insight'.</div>
</div>
<div style="display: flex; align-items: center; border-top: 1px solid var(--border); padding: 8px; background: var(--bg-base);">
<span style="color: var(--accent); margin-right: 8px; font-weight: 600;">></span>
<input class="console-input" type="text" style="flex: 1; background: transparent; border: none; color: var(--text-main); font-family: var(--font-mono); outline: none;" placeholder="Evaluate JavaScript..." />
</div>
</div>`;
const output = win.content.querySelector('.console-output');
const input = win.content.querySelector('.console-input');
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && input.value) {
const code = input.value; input.value = '';
output.innerHTML += `<div style="color: var(--text-muted); margin-top: 8px;">> ${code}</div>`;
try {
const result = new Function('insight', `return eval(${JSON.stringify(code)})`)(this.insight);
output.innerHTML += `<div style="color: #A78BFA;">< ${String(result)}</div>`;
} catch (err) {
output.innerHTML += `<div style="color: #F87171;">${String(err)}</div>`;
}
output.scrollTop = output.scrollHeight;
}
});
}
}
/**
* =========================================================================
* 11. COMMAND PALETTE & SETTINGS & PLUGINS
* =========================================================================
*/
class CommandPalette extends Module {
initUI() {
this.commands = [];
this.filtered = [];
this.selectedIndex = 0;
this.el = document.createElement('div');
this.el.className = 'cmd-overlay';
this.el.style.display = 'none';
this.container = document.createElement('div');
this.container.className = 'cmd-palette';
this.input = document.createElement('input');
this.input.className = 'cmd-input';
this.input.placeholder = 'Search Insight...';
this.list = document.createElement('div');
this.list.className = 'cmd-list';
this.container.appendChild(this.input);
this.container.appendChild(this.list);
this.el.appendChild(this.container);
this.insight.ui.shadowRoot.appendChild(this.el);
window.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'p') {
e.preventDefault();
this.toggle();
}
}, true);
this.input.addEventListener('input', () => this.filter(this.input.value));
this.input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') { e.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, this.filtered.length - 1); this.renderList(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); this.selectedIndex = Math.max(this.selectedIndex - 1, 0); this.renderList(); }
else if (e.key === 'Enter') { e.preventDefault(); const cmd = this.filtered[this.selectedIndex]; if (cmd) { this.hide(); cmd.action(); } }
else if (e.key === 'Escape') this.hide();
});
this.el.addEventListener('click', (e) => { if (e.target === this.el) this.hide(); });
// Helpful default guide command
this.registerCommand('Open Developer Guide', 'help-circle', () => {
const win = this.insight.ui.createWindow('dev_guide', 'Developer Guide', 'help-circle');
win.el.style.width = '500px';
win.content.innerHTML = `
<div style="padding: 16px; line-height: 1.6; color: var(--text-muted);">
<h2 style="color: var(--text-main); margin-top: 0;">Scripting Insight</h2>
<p>Interact with applications using the <code style="color: var(--accent);">insight</code> global object via Developer Console.</p>
<h3 style="color: var(--text-main);">Quick Tips:</h3>
<ul style="padding-left: 20px;">
<li>Assets: <code>insight.modules.detector.textures</code></li>
<li>Inspect: <code>insight.emit('inspect-object', mesh)</code></li>
<li>Commands: <code>insight.modules.commands.registerCommand(...)</code></li>
</ul>
</div>`;
});
}
registerCommand(name, icon, action) { this.commands.push({ name, icon, action }); }
toggle() { if (this.el.style.display === 'none') this.show(); else this.hide(); }
show() { this.el.style.display = 'flex'; this.input.value = ''; this.filter(''); this.input.focus(); }
hide() { this.el.style.display = 'none'; }
filter(q) {
this.filtered = this.commands.filter(c => c.name.toLowerCase().includes(q.toLowerCase()));
this.selectedIndex = 0;
this.renderList();
}
renderList() {
this.list.innerHTML = '';
this.filtered.forEach((cmd, idx) => {
const item = document.createElement('div');
item.className = `cmd-item ${idx === this.selectedIndex ? 'selected' : ''}`;
item.innerHTML = `<i data-lucide="${cmd.icon}"></i> <span>${cmd.name}</span>`;
item.onclick = () => { this.hide(); cmd.action(); };
item.onmouseenter = () => { this.selectedIndex = idx; this.renderList(); };
this.list.appendChild(item);
});
this.insight.ui.refreshIcons(this.list);
const sel = this.list.children[this.selectedIndex];
if (sel) sel.scrollIntoView({ block: 'nearest' });
}
}
class SettingsManager extends Module {
initUI() { this.insight.commands.registerCommand('Settings', 'settings', () => this.openWindow()); }
openWindow() {
const win = this.insight.ui.createWindow('settings', 'Settings', 'settings');
win.content.innerHTML = `
<div style="padding: 16px; color: var(--text-muted);">
<h3 style="margin: 0 0 16px 0; font-size: 14px; font-weight: 500; color: var(--text-main);">Preferences</h3>
<label style="display: flex; align-items: center; gap: 8px;"><input type="checkbox" checked disabled /> Dark Theme (Zinc)</label><br>
<label style="display: flex; align-items: center; gap: 8px;"><input type="checkbox" checked disabled /> Auto-hook Three.js Prototypes</label><br>
<button class="btn" style="margin-top: 16px;" onclick="localStorage.clear(); alert('Local storage cleared. Refresh page to reset windows.');">Reset Window Positions</button>
</div>`;
}
}
class PluginManager extends Module {
init() {
window.InsightAPI = {
registerPlugin: (plugin) => {
if (plugin && typeof plugin.init === 'function') {
try {
plugin.init(this.insight);
console.log(`[Insight] Loaded external plugin: ${plugin.name} v${plugin.version}`);
if (this.insight.ui) this.insight.ui.showToast(`Plugin Loaded: ${plugin.name}`);
} catch (err) { console.error(`[Insight] Failed to load plugin ${plugin.name}:`, err); }
}
}
};
}
}
/**
* =========================================================================
* CORE ORCHESTRATOR
* =========================================================================
*/
class InsightCore extends EventEmitter {
constructor() {
super();
this.modules = {};
window.insight = this;
this.ui = new UIFramework(this);
// Register All Modules
this.registerModule('detector', new ThreeDetector(this));
this.registerModule('performance', new PerformanceMonitor(this));
this.registerModule('commands', new CommandPalette(this));
this.registerModule('raycaster', new PickerTool(this));
this.registerModule('scene', new SceneExplorer(this));
this.registerModule('inspector', new EntityInspector(this));
this.registerModule('assets', new AssetExplorer(this));
this.registerModule('runtimeGraph', new RuntimeObjectExplorer(this));
this.registerModule('network', new NetworkAnalyzer(this));
this.registerModule('console', new DeveloperConsole(this));
this.registerModule('tracer', new FunctionTracer(this));
this.registerModule('settings', new SettingsManager(this));
this.registerModule('plugins', new PluginManager(this));
this.initRuntime();
}
registerModule(id, instance) {
this.modules[id] = instance;
}
initRuntime() {
for (const key in this.modules) {
if (this.modules[key].init) this.modules[key].init();
}
}
initUI() {
this.ui.mount();
for (const key in this.modules) {
if (this.modules[key].initUI) this.modules[key].initUI();
}
this.commands = this.modules.commands;
console.log('%c[Insight Platform] Advanced Editor Mode Ready.', 'color: #3B82F6; font-weight: bold;');
setTimeout(() => {
this.ui.showToast('Insight Platform Ready. Press Ctrl+Shift+P to open Command Palette.');
}, 500);
}
}
// Bootstrap
const insightPlatform = new InsightCore();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => insightPlatform.initUI());
} else {
insightPlatform.initUI();
}
})();