Greasy Fork is available in English.
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.2
// @description Professional-grade Tampermonkey userscript for runtime analysis, debugging, exploration, and research of Three.js games and applications.
// @author Insight Platform & F1xL1T & FunWithScripts
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
/**
* =========================================================================
* UTILITIES & MEMORY SAFETY
* =========================================================================
*/
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); // Return cleanup function
}
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();
}
}
/**
* =========================================================================
* UI FRAMEWORK & COMPONENTS
* =========================================================================
*/
class UIWindow {
constructor(ui, title, iconName) {
this.ui = ui;
this.el = document.createElement('div');
this.el.className = 'insight-window';
this.el.style.left = '100px';
this.el.style.top = '100px';
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.makeDraggable();
this.ui.refreshIcons(this.header);
}
makeDraggable() {
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.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);
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);
});
}
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;
--accent-hover: #60A5FA;
--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;
transition: box-shadow 0.2s ease;
}
.insight-window:active {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.6);
}
.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:active { cursor: grabbing; }
.insight-header .title { font-weight: 500; flex-grow: 1; letter-spacing: 0.02em; }
.insight-header .close-btn { cursor: pointer; color: var(--text-muted); transition: color 0.15s; }
.insight-header .close-btn:hover { color: #F87171; }
.window-content {
flex: 1;
overflow: auto;
height: calc(100% - 33px);
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;
transition: all 0.15s ease;
}
.btn:hover { background: var(--border); }
.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;
align-items: flex-start;
padding-top: 15vh;
z-index: 10000;
animation: fadeIn 0.15s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.98); }
to { opacity: 1; transform: scale(1); }
}
.cmd-palette {
width: 600px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
overflow: hidden;
}
.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;
font-family: var(--font-sans);
}
.cmd-list {
max-height: 300px;
overflow-y: auto;
}
.cmd-item {
padding: 12px 16px;
display: flex;
align-items: center;
cursor: pointer;
color: var(--text-muted);
}
.cmd-item i { margin-right: 12px; }
.cmd-item.selected {
background: var(--bg-hover);
color: var(--text-main);
}
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);
}
.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 {
background: var(--bg-hover);
color: var(--text-main);
}
.sidebar-item.active {
background: var(--bg-hover);
color: var(--text-main);
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(title, iconName) {
return new UIWindow(this, 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;
opacity: 0; transform: translateY(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
`;
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);
requestAnimationFrame(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
});
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(10px)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
}
/**
* =========================================================================
* PERFORMANCE & DETECTOR MODULE (v1.0.2 Upgrade)
* =========================================================================
*/
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.setupHooks();
}
initUI() {
this.insight.commands.registerCommand('Force Scan Global Context', 'radar', () => {
this.scanGlobal();
this.insight.ui.showToast(`Forced Scan: ${this.scenes.size} Scenes, ${this.textures.size} Textures.`);
});
}
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;
THREE._insightHooked = true;
const proxySubclasses = (baseName, callback) => {
Object.keys(THREE).forEach(key => {
if (key.includes(baseName) || key === baseName) {
this.hookConstructor(THREE, key, callback);
}
});
};
// Intelligent Hooking instead of setInterval Global scanning
proxySubclasses('Scene', (obj) => { this.scenes.add(obj); this.emit('asset-added', { type: 'scene', obj }); });
proxySubclasses('WebGLRenderer', (obj) => { this.renderers.add(obj); this.emit('asset-added', { type: 'renderer', obj }); });
proxySubclasses('Texture', (obj) => { this.textures.add(obj); this.emit('asset-added', { type: 'texture', obj }); });
proxySubclasses('Material', (obj) => { this.materials.add(obj); this.emit('asset-added', { type: 'material', obj }); });
proxySubclasses('Geometry', (obj) => { this.geometries.add(obj); this.emit('asset-added', { type: 'geometry', obj }); });
proxySubclasses('Camera', (obj) => { this.cameras.add(obj); this.emit('asset-added', { type: 'camera', obj }); });
// Hook the renderer's render loop to capture previously unhooked scenes
if (THREE.WebGLRenderer) {
const origRender = THREE.WebGLRenderer.prototype.render;
const self = this;
THREE.WebGLRenderer.prototype.render = function(scene, 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 }); }
if (camera && !self.cameras.has(camera)) { self.cameras.add(camera); self.emit('asset-added', { type: 'camera', obj: camera }); }
return origRender.apply(this, arguments);
};
}
console.log('[Insight] Intelligent Three.js hooking initialized.');
}
hookConstructor(namespace, className, callback) {
if (!namespace[className]) return;
const Orig = namespace[className];
if (Orig.__insightHooked) return;
try {
const hooked = new Proxy(Orig, {
construct(target, args) {
const obj = new target(...args);
callback(obj);
return obj;
}
});
hooked.__insightHooked = true;
namespace[className] = hooked;
} catch (e) {
// Fallback for non-constructable properties
}
}
scanGlobal() {
// Fallback scanner for already initialized objects
const traverse = (obj) => {
if (!obj || typeof obj !== 'object') return;
if (obj.isScene) this.scenes.add(obj);
if (obj.isCamera) this.cameras.add(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);
}
}
/**
* =========================================================================
* RUNTIME OBJECT EXPLORER (v1.0.2 Addition)
* =========================================================================
*/
class RuntimeObjectExplorer extends Module {
initUI() {
this.insight.commands.registerCommand('Runtime Object Graph', 'network', () => this.openWindow());
}
openWindow() {
const win = this.insight.ui.createWindow('Runtime Graph', 'network');
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;
};
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');
// Map top level scene children
s.children.forEach(c => {
const cNode = addNode(c, c.name || c.type, 'object');
addEdge(sNode, cNode, 'child');
});
});
});
// Reset layout
transform = { x: canvas.width / 2, y: canvas.height / 2, scale: 1 };
};
buildGraph();
// 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;
// Physics step (Spring layout)
if (!dragNode) {
// Repulsion
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;
}
}
}
// Attraction
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;
});
// Integration
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);
}
}
/**
* =========================================================================
* ASSET EXPLORER (v1.0.2 Addition)
* =========================================================================
*/
class AssetExplorer extends Module {
initUI() {
this.insight.commands.registerCommand('Asset Explorer', 'package', () => this.openWindow());
}
openWindow() {
const win = this.insight.ui.createWindow('Asset Explorer', 'package');
win.el.style.width = '700px';
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>
<div class="main-content">
<div style="padding: 8px; border-bottom: 1px solid var(--border);">
<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: 8px; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); align-content: start;">
</div>
</div>
</div>
`;
let currentTab = 'textures';
let searchQuery = '';
const listEl = win.content.querySelector('#asset-list');
const searchEl = win.content.querySelector('#asset-search');
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);
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; font-size: 11px;';
let meta = '';
if (currentTab === 'textures') {
meta = `<div>Res: ${item.image ? item.image.width+'x'+item.image.height : 'Unknown'}</div>
<div>Format: ${item.format}</div>`;
} else if (currentTab === 'materials') {
meta = `<div>Type: ${item.type}</div>
<div>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>Verts: ${verts.toLocaleString()}</div>
<div>Tris: ${Math.floor(tris).toLocaleString()}</div>`;
}
card.innerHTML = `
<div style="font-weight: 600; font-size: 12px; margin-bottom: 6px; color: var(--accent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${name}">${name}</div>
<div style="color: var(--text-muted); line-height: 1.5; font-family: var(--font-mono);">${meta}</div>
<div style="color: #666; font-size: 9px; margin-top: 6px;">${item.uuid.substr(0,8)}</div>
`;
listEl.appendChild(card);
});
};
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();
});
});
searchEl.addEventListener('input', (e) => {
searchQuery = e.target.value;
renderItems();
});
// Live updates
const cleanup = this.insight.modules.detector.on('asset-added', (data) => {
if (data.type + 's' === currentTab || (data.type === 'geometry' && currentTab === 'geometries')) {
renderItems();
}
});
win.onClose(cleanup);
renderItems();
this.insight.ui.refreshIcons(win.content);
}
}
/**
* =========================================================================
* ENTITY INSPECTOR (v1.0.2 Upgrade - Live Mode + Deep Inspect)
* =========================================================================
*/
class EntityInspector extends Module {
initUI() {
this.insight.commands.registerCommand('Open 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', 'sliders');
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 in the Scene Explorer to inspect.</div>';
return;
}
const createRow = (label, refKey, staticVal = null) => {
const id = 'ref_' + Math.random().toString(36).substr(2, 9);
if (refKey) this.liveRefs[refKey] = id;
return `<div style="display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 12px;">
<span style="color: var(--text-muted);">${label}</span>
<span id="${id}" style="font-family: var(--font-mono); color: var(--text-main);">${staticVal !== null ? staticVal : '-'}</span>
</div>`;
};
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; letter-spacing: 0.05em;">${title}</div>
${inner}
</div>
`;
let html = ``;
html += section('Information',
createRow('Name', null, `<span style="color: var(--accent);">${obj.name || 'N/A'}</span>`) +
createRow('Type', null, obj.type) +
createRow('UUID', null, `<span style="font-size: 10px;">${obj.uuid}</span>`)
);
if (obj.position) {
html += section('Transform',
createRow('Position', 'pos') +
createRow('Rotation', 'rot') +
createRow('Scale', 'scale')
);
}
if (obj.geometry) {
const geo = obj.geometry;
const verts = geo.attributes?.position?.count || 0;
const tris = geo.index ? geo.index.count / 3 : verts / 3;
html += section('Geometry',
createRow('Type', null, geo.type) +
createRow('Vertices', null, verts.toLocaleString()) +
createRow('Triangles', null, Math.floor(tris).toLocaleString()) +
createRow('Bounding Sphere', 'bounds')
);
}
if (obj.material) {
const mat = obj.material;
const matInfo = Array.isArray(mat) ? `Array (${mat.length})` : mat.type;
html += section('Material',
createRow('Type', null, matInfo) +
createRow('Transparent', null, mat.transparent) +
createRow('Opacity', 'opacity')
);
}
html += section('Hierarchy & Renderer',
createRow('Visible', 'visible') +
createRow('Render Order', null, obj.renderOrder) +
createRow('Children', null, obj.children?.length || 0) +
createRow('Parent', null, obj.parent ? obj.parent.type : 'None')
);
content.innerHTML = html;
// Map IDs to actual elements for quick rAF updates
for (const key in this.liveRefs) {
this.liveRefs[key] = content.querySelector('#' + this.liveRefs[key]);
}
}
updateLiveValues() {
const obj = this.activeObject;
const refs = this.liveRefs;
const fn = n => typeof n === 'number' ? n.toFixed(3) : n;
if (refs.pos && obj.position) refs.pos.textContent = `${fn(obj.position.x)}, ${fn(obj.position.y)}, ${fn(obj.position.z)}`;
if (refs.rot && obj.rotation) refs.rot.textContent = `${fn(obj.rotation.x)}, ${fn(obj.rotation.y)}, ${fn(obj.rotation.z)}`;
if (refs.scale && obj.scale) refs.scale.textContent = `${fn(obj.scale.x)}, ${fn(obj.scale.y)}, ${fn(obj.scale.z)}`;
if (refs.visible) refs.visible.textContent = obj.visible ? 'True' : 'False';
if (refs.opacity && obj.material) refs.opacity.textContent = Array.isArray(obj.material) ? '-' : fn(obj.material.opacity);
if (refs.bounds && obj.geometry && obj.geometry.boundingSphere) {
refs.bounds.textContent = `Rad: ${fn(obj.geometry.boundingSphere.radius)}`;
}
}
}
/**
* =========================================================================
* PRE-EXISTING MODULES (Maintained & Integrated)
* =========================================================================
*/
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 commands...';
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);
this.setupEvents();
}
registerCommand(name, icon, action) {
this.commands.push({ name, icon, action });
}
setupEvents() {
window.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'p') {
e.preventDefault(); e.stopPropagation();
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(); });
}
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(query) {
query = query.toLowerCase();
this.filtered = this.commands.filter(cmd => cmd.name.toLowerCase().includes(query));
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.addEventListener('click', () => { this.hide(); cmd.action(); });
item.addEventListener('mouseenter', () => { this.selectedIndex = idx; this.renderList(); });
this.list.appendChild(item);
});
this.insight.ui.refreshIcons(this.list);
const selectedEl = this.list.children[this.selectedIndex];
if (selectedEl) selectedEl.scrollIntoView({ block: 'nearest' });
}
}
class SceneExplorer extends Module {
initUI() {
this.insight.commands.registerCommand('Scene Hierarchy', 'layers', () => this.openWindow());
}
openWindow() {
const win = this.insight.ui.createWindow('Hierarchy', 'layers');
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()));
// Auto Update via MutationObserver pattern on the Set size is heavy, rely on explicit hook events
const cleanup = this.insight.modules.detector.on('asset-added', (data) => {
if (data.type === 'scene') this.renderTree(treeContainer, search.value.toLowerCase());
});
win.onClose(cleanup);
this.renderTree(treeContainer, '');
}
getIcon(type) {
switch(type) {
case 'Scene': return 'globe';
case 'PerspectiveCamera': case 'OrthographicCamera': return 'camera';
case 'Mesh': return 'box';
case 'PointLight': case 'DirectionalLight': return 'sun';
case 'Group': return 'folder';
default: return 'cuboid';
}
}
renderTree(container, filterText) {
container.innerHTML = '';
const scenes = Array.from(this.insight.modules.detector.scenes);
if (scenes.length === 0) {
container.innerHTML = '<div style="color: var(--text-muted); text-align: center; margin-top: 20px;">No scenes detected.</div>';
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';
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>`;
row.innerHTML = `${chevronHtml}<i data-lucide="${this.getIcon(object.type)}" style="width: 14px; margin-right: 6px; color: var(--text-muted);"></i><span style="font-size: 13px; color: ${object.visible ? 'var(--text-main)' : 'var(--text-muted)'};">${object.name || object.type || 'Object3D'}</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"]').addEventListener('click', (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);
}
}
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('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 });
const cleanup = this.on('stats', updateUI);
win.onClose(cleanup);
}
}
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(); // Memory Safety Limit
this.emit('new-request', req);
}
openWindow() {
const win = this.insight.ui.createWindow('Network', 'globe');
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);
}
}
class DeveloperConsole extends Module {
initUI() { this.insight.commands.registerCommand('Developer Console', 'terminal', () => this.openWindow()); }
openWindow() {
const win = this.insight.ui.createWindow('Console', 'terminal');
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; border-radius: 4px; 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); font-size: 13px; 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;
}
});
}
}
class FunctionTracer extends Module {
initUI() {
this.traces = [];
this.insight.commands.registerCommand('Function Tracer', 'code', () => this.openWindow());
}
trace(obj, methodName) {
const orig = obj[methodName];
const self = this;
obj[methodName] = function(...args) {
const start = performance.now();
const res = orig.apply(this, args);
self.traces.push({ method: methodName, duration: performance.now() - start });
if (self.traces.length > 500) self.traces.shift(); // Memory safety limit
self.emit('trace', self.traces[self.traces.length-1]);
return res;
};
this.insight.ui.showToast(`Tracing active for ${methodName}`);
}
openWindow() {
const win = this.insight.ui.createWindow('Function Tracer', 'code');
win.content.innerHTML = `<div style="padding: 16px; color: var(--text-muted); line-height: 1.5;">
<strong>Trace Instructions:</strong><br><br>
Use the Developer Console to initiate traces on any accessible prototype or object instance.<br><br>
<code>insight.modules.tracer.trace(THREE.Vector3.prototype, 'normalize');</code>
</div>`;
}
}
class SettingsManager extends Module {
initUI() { this.insight.commands.registerCommand('Settings', 'settings', () => this.openWindow()); }
openWindow() {
const win = this.insight.ui.createWindow('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>
</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 Modules
this.registerModule('settings', new SettingsManager(this));
this.registerModule('plugins', new PluginManager(this));
this.registerModule('detector', new ThreeDetector(this));
this.registerModule('network', new NetworkAnalyzer(this));
this.registerModule('performance', new PerformanceMonitor(this));
this.registerModule('commands', new CommandPalette(this));
this.registerModule('scene', new SceneExplorer(this));
this.registerModule('inspector', new EntityInspector(this));
this.registerModule('console', new DeveloperConsole(this));
this.registerModule('tracer', new FunctionTracer(this));
// v1.0.2 New Modules
this.registerModule('runtimeGraph', new RuntimeObjectExplorer(this));
this.registerModule('assets', new AssetExplorer(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] Professional Runtime Analysis Studio 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();
}
})();