VISIGENCE
STUDIO · 3D SCENE EDITOR
Initializing…
RENDER
Scene
📦 Box
🔮 Sphere
🥫 Cylinder
⬜ Plane
🍩 Torus
🔺 Cone
⦾ Empty
💊 Capsule
0 fps 0 meshes
Translate
VISIGENCE STUDIO
WebGL is not available in this preview.
Open in a new tab or deploy for full 3D engine.
All panels — Inspector, Script Editor, Console — are fully functional.
Console
No output yet.
Inspector
Select an object
in the scene
Project
Lifecycle
onAttach(mesh)
onUpdate(dt: number)
onDetach()
Globals
BABYLON — full API
scene — Scene
console → Console
@exposed
Put // @exposed
above a field to edit
it live in Inspector
📄
Select a file or create
a new script
📄 untitled.ts
Assign in Inspector → Scripts
⚙ Studio Settings
◈ Post-Processing
Bloom
Bloom Threshold
0.70
Bloom Intensity
0.30

Vignette
Vignette Weight
2.50

Contrast
1.10
Exposure
1.00
MSAA Samples
☀ Lighting
Sun Intensity
1.40
Ambient Intensity
0.30

🎥 Camera
Field of View
60°
Wheel Precision
50

💠 Shadow
Shadow Quality
Soft Shadows
📝 Editor Preferences
Font Size
Font Ligatures
Minimap
Word Wrap
Auto Format on Paste
ℹ About Visigence Studio
Visigence Studio is a professional browser-based 3D scene editor powered by BabylonJS. Build, script, and export 3D scenes without any build step.

Version1.0.0
EngineDetecting…
BabylonJS
AuthorOmry Damari
LicenseProprietary

© 2026 Omry Damari. All Rights Reserved.
FocusF
Rename
DuplicateCtrl+D
👁Toggle Visibility
DeleteDel
`; download('VisigenceStudio_Export.html', html, 'text/html'); UINotif.show('HTML exported successfully', 'success'); logToConsole('info', 'Exported standalone HTML scene', 'IDE'); } function exportJSON() { const meshes = getMeshSnapshot(); const scripts = {}; ProjectFiles.getAllScriptNames().forEach(n => { scripts[n] = ProjectFiles.getContent(n); }); const project = { $schema: 'https://visigence.io/schema/scene/v1.json', version: '1.0.0', studio: 'Visigence Studio', author: 'Omry Damari', exportedAt: new Date().toISOString(), scene: { clearColor: [0.012, 0.035, 0.07, 1], postProcessing: { bloom:true, bloomThreshold:0.7, bloomWeight:0.3, vignette:true, vignetteWeight:2.5, contrast:1.1, exposure:1.0, msaa:4 }, meshes, }, scripts, meshScriptAssignments: State.get('meshScriptAssignments') || {}, }; download('scene.json', JSON.stringify(project, null, 2), 'application/json'); UINotif.show('JSON exported', 'success'); logToConsole('info', 'Exported scene JSON', 'IDE'); } function exportSourceZip() { const files = {}; ProjectFiles.getAllScriptNames().forEach(n => { files[`visigence-studio/scripts/${n}`] = ProjectFiles.getContent(n); }); files['visigence-studio/scene.json'] = JSON.stringify({ scene: { meshes: getMeshSnapshot() }, author: 'Omry Damari' }, null, 2); files['visigence-studio/README.md'] = `# Visigence Studio — Project Export\n\n© 2026 Omry Damari\n\n## Script Lifecycle\n- **onAttach(mesh)** — called when Play starts\n- **onUpdate(dt)** — called every frame (dt = seconds)\n- **onDetach()** — called when Play stops\n\n## Globals in scripts\n- \`BABYLON\` — full BabylonJS API\n- \`scene\` — active Scene\n- \`console\` — routes to editor Console\n\n## @exposed decorator\nPrefix a field with \`// @exposed\` to edit it live in the Inspector.\n`; const zipBytes = _buildZip(files); const blob = new Blob([zipBytes], { type: 'application/zip' }); const url = URL.createObjectURL(blob); const a = Object.assign(document.createElement('a'), { href: url, download: 'visigence-studio.zip' }); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); UINotif.show('Source ZIP downloaded', 'success'); logToConsole('info', 'Source ZIP exported', 'IDE'); } // Minimal stored (no-compression) ZIP writer function _buildZip(files) { const enc = new TextEncoder(); const crc32table = (() => { const t = new Uint32Array(256); for (let i=0; i<256; i++) { let c=i; for (let j=0; j<8; j++) c = (c&1) ? (0xEDB88320^(c>>>1)) : (c>>>1); t[i]=c; } return t; })(); const crc32 = buf => { let c = 0xFFFFFFFF; for (let i=0; i>>8); return (c^0xFFFFFFFF) >>> 0; }; const u16 = n => [n&0xff,(n>>8)&0xff]; const u32 = n => [n&0xff,(n>>8)&0xff,(n>>16)&0xff,(n>>24)&0xff]; const now = new Date(); const dosTime = ((now.getHours()<<11)|(now.getMinutes()<<5)|(now.getSeconds()>>1)); const dosDate = (((now.getFullYear()-1980)<<9)|((now.getMonth()+1)<<5)|now.getDate()); const chunks = []; const centralDirs = []; let offset = 0; for (const [name, content] of Object.entries(files)) { const nBytes = enc.encode(name); const dBytes = typeof content === 'string' ? enc.encode(content) : content; const crc = crc32(dBytes); const sz = dBytes.length; const lh = new Uint8Array([ 0x50,0x4B,0x03,0x04, 20,0, 0,0, 0,0, ...u16(dosTime), ...u16(dosDate), ...u32(crc), ...u32(sz), ...u32(sz), ...u16(nBytes.length), 0,0, ...nBytes, ]); const entryOffset = offset; chunks.push(lh, dBytes); offset += lh.length + dBytes.length; const cd = new Uint8Array([ 0x50,0x4B,0x01,0x02, 20,0, 20,0, 0,0, 0,0, ...u16(dosTime), ...u16(dosDate), ...u32(crc), ...u32(sz), ...u32(sz), ...u16(nBytes.length), 0,0, 0,0, 0,0, 0,0,0,0, 0,0,0,0, ...u32(entryOffset), ...nBytes, ]); centralDirs.push(cd); } const cdStart = offset; let cdSize = 0; centralDirs.forEach(cd => { chunks.push(cd); cdSize += cd.length; }); const eocd = new Uint8Array([ 0x50,0x4B,0x05,0x06, 0,0, 0,0, ...u16(centralDirs.length), ...u16(centralDirs.length), ...u32(cdSize), ...u32(cdStart), 0,0, ]); chunks.push(eocd); const total = chunks.reduce((s,c) => s+c.length, 0); const out = new Uint8Array(total); let pos = 0; for (const chunk of chunks) { out.set(chunk, pos); pos += chunk.length; } return out; } return { exportHTML, exportJSON, exportSourceZip }; })(); /* ──────────────────────────────────────────────────────────── §12 UI — INSPECTOR ──────────────────────────────────────────────────────────── */ const UIInspector = (() => { let _pollId = null; let _scriptDropdown = null; function refresh() { const name = State.get('selectedMeshName'); const empty = document.getElementById('inspector-empty'); const content = document.getElementById('inspector-content'); clearInterval(_pollId); if (!name) { empty.style.display = 'flex'; content.style.display = 'none'; content.innerHTML = ''; return; } empty.style.display = 'none'; content.style.display = 'block'; content.innerHTML = ''; _renderContent(content, name); } function _renderContent(root, name) { const sm = Engine.getSceneManager(); const mesh = sm ? sm.getMesh(name) : null; // Object block const block = document.createElement('div'); block.className = 'insp-object-block'; const baseName = name.replace(/_\d+$/, ''); block.innerHTML = `
Selected Object
${esc(name)}
${baseName} · Mesh
Pos Vis
`; block.querySelector('.insp-rename-btn').onclick = () => promptRename(name); root.appendChild(block); // Transform section const tfBody = _makeSection(root, '⊕ Transform', true); if (mesh) { _vec3(tfBody, 'Position', [mesh.position.x, mesh.position.y, mesh.position.z], 0.1, v => { sm.setPos(name, v[0], v[1], v[2]); }); _vec3(tfBody, 'Rotation °', [mesh.rotation.x*180/Math.PI, mesh.rotation.y*180/Math.PI, mesh.rotation.z*180/Math.PI], 1, v => { sm.setRot(name, v[0]*Math.PI/180, v[1]*Math.PI/180, v[2]*Math.PI/180); }); _vec3(tfBody, 'Scale', [mesh.scaling.x, mesh.scaling.y, mesh.scaling.z], 0.01, v => { sm.setScale(name, v[0], v[1], v[2]); }); // Reset button const resetBtn = document.createElement('button'); resetBtn.className = 'insp-action-btn'; resetBtn.textContent = '↺ Reset Transform'; resetBtn.onclick = () => { sm.setPos(name, 0, 1, 0); sm.setRot(name, 0, 0, 0); sm.setScale(name, 1, 1, 1); refresh(); }; tfBody.appendChild(resetBtn); } // Material section const matBody = _makeSection(root, '◈ Material', true); const mp = sm ? sm.getMaterialProps(name) : null; if (mp) { _colorRow(matBody, 'Albedo Color', mp.color, (r,g,b) => sm.setMaterialColor(name, r, g, b)); _colorRow(matBody, 'Emissive', mp.emissive || [0,0,0], (r,g,b) => sm.setMaterialEmissive(name, r, g, b)); if (mp.isPBR) { _slider(matBody, 'Metallic', mp.metallic, 0, 1, v => sm.setMaterialPBR(name, v, sm.getMaterialProps(name)?.roughness ?? 0.5)); _slider(matBody, 'Roughness', mp.roughness, 0, 1, v => sm.setMaterialPBR(name, sm.getMaterialProps(name)?.metallic ?? 0, v)); _slider(matBody, 'Opacity', mp.alpha ?? 1, 0, 1, v => sm.setMaterialAlpha(name, v)); } } else { const p = document.createElement('div'); p.style.cssText = 'font-size:10px;color:rgba(255,255,255,0.2);'; p.textContent = 'No material'; matBody.appendChild(p); } // Rendering / Visibility section const renBody = _makeSection(root, '👁 Rendering', false); const visRow = document.createElement('div'); visRow.className = 'toggle-row'; const isVis = mesh ? mesh.isVisible : true; visRow.innerHTML = `Visible
`; visRow.querySelector('.toggle-switch').onclick = function() { this.classList.toggle('on'); const v = this.classList.contains('on'); sm?.setMeshVisible(name, v); document.getElementById('stat-vis').textContent = v ? '✓' : '✗'; }; renBody.appendChild(visRow); const castRow = document.createElement('div'); castRow.className = 'toggle-row'; castRow.innerHTML = `Cast Shadows
`; renBody.appendChild(castRow); // Scripts section const scrBody = _makeSection(root, '⚙ Scripts', true); _renderScripts(scrBody, name); // Start live transform polling if (mesh) _startPoll(name, tfBody); } function _startPoll(name, container) { clearInterval(_pollId); _pollId = setInterval(() => { const sm = Engine.getSceneManager(); const m = sm ? sm.getMesh(name) : null; if (!m) { clearInterval(_pollId); return; } const inputs = container.querySelectorAll('.num-input'); if (inputs.length < 9) return; const vals = [ m.position.x,m.position.y,m.position.z, m.rotation.x*180/Math.PI,m.rotation.y*180/Math.PI,m.rotation.z*180/Math.PI, m.scaling.x,m.scaling.y,m.scaling.z, ]; vals.forEach((v, i) => { if (inputs[i] !== document.activeElement) inputs[i].value = +v.toFixed(3); }); // Update pos stat const sp = document.getElementById('stat-pos'); if (sp) sp.textContent = `${m.position.x.toFixed(1)},${m.position.y.toFixed(1)},${m.position.z.toFixed(1)}`; }, 100); } function _makeSection(parent, label, open) { const sec = document.createElement('div'); sec.className = 'insp-section'; const hdr = document.createElement('div'); hdr.className = 'insp-section-header'; const ch = ``; hdr.innerHTML = label + ch; const body = document.createElement('div'); body.className = 'insp-section-body' + (open ? ' open' : ''); hdr.onclick = () => { body.classList.toggle('open'); hdr.querySelector('.insp-section-chevron').classList.toggle('open'); }; sec.appendChild(hdr); sec.appendChild(body); parent.appendChild(sec); return body; } function _vec3(parent, label, values, step, onChange) { const row = document.createElement('div'); row.className = 'vec3-row'; row.innerHTML = `
${label}
${['X','Y','Z'].map((ax,i)=>`
${ax}
`).join('')}
`; const inputs = row.querySelectorAll('input'); inputs.forEach((inp, i) => { inp.addEventListener('change', () => { const arr = [...inputs].map(x => parseFloat(x.value) || 0); onChange(arr); }); inp.addEventListener('input', () => { const v = parseFloat(inp.value); if (!isNaN(v)) { const arr = [...inputs].map((x,j) => j===i ? v : parseFloat(x.value)||0); onChange(arr); } }); }); parent.appendChild(row); } function _colorRow(parent, label, rgb, onChange) { const row = document.createElement('div'); row.className = 'mat-row'; const hex = rgbToHex(...rgb); row.innerHTML = `${label}
${hex}
`; const ci = row.querySelector('input[type=color]'); const hs = row.querySelector('.color-hex'); ci.addEventListener('input', () => { hs.textContent = ci.value; onChange(...hexToRgb(ci.value)); }); parent.appendChild(row); } function _slider(parent, label, value, min, max, onChange) { const row = document.createElement('div'); row.className = 'slider-row'; row.innerHTML = `
${label} ${value.toFixed(2)}
`; const sl = row.querySelector('input'); const sv = row.querySelector('.slider-val'); sl.addEventListener('input', () => { const v=parseFloat(sl.value); sv.textContent=v.toFixed(2); onChange(v); }); parent.appendChild(row); } function _renderScripts(parent, meshName) { const assignments = State.get('meshScriptAssignments') || {}; const assigned = assignments[meshName] || []; const isPlaying = State.get('isPlaying'); const sm = Engine.getSceneManager(); if (assigned.length === 0 && !isPlaying) { const hint = document.createElement('div'); hint.style.cssText = 'font-size:9.5px;color:rgba(255,255,255,0.18);margin-bottom:6px;'; hint.textContent = 'No scripts — use + to assign'; parent.appendChild(hint); } assigned.forEach(scriptName => { const chip = document.createElement('div'); chip.className = 'script-chip'; const instances = sm ? sm.scriptRuntime.getInstances(meshName) : []; const inst = instances.find(x => x.scriptName === scriptName); const dotClass = !isPlaying ? '' : (inst?.hasError ? 'error' : 'running'); const chipRow = document.createElement('div'); chipRow.className = 'script-chip-row'; chipRow.innerHTML = `
${esc(scriptName)}
`; chipRow.querySelector('[data-open]').onclick = () => { document.querySelectorAll('.mb-tab').forEach(t => { if (t.dataset.tab==='scripts') t.click(); }); UIScriptEditor.openFile(scriptName); }; chipRow.querySelector('[data-rm]').onclick = () => { const a = { ...State.get('meshScriptAssignments') }; a[meshName] = (a[meshName]||[]).filter(s => s!==scriptName); State.set('meshScriptAssignments', a); refresh(); }; chip.appendChild(chipRow); // Exposed vars (only in play mode) if (isPlaying && inst && !inst.hasError && inst.exposedVars.length > 0) { const evBody = document.createElement('div'); evBody.className = 'ev-body'; inst.exposedVars.forEach(ev => { const r = document.createElement('div'); r.className = 'exposed-var-row'; if (ev.type === 'boolean') { r.innerHTML = `${esc(ev.name)}`; r.querySelector('input').onchange = e => { sm.scriptRuntime.setVar(meshName, scriptName, ev.name, e.target.checked); }; } else { r.innerHTML = `${esc(ev.name)}`; r.querySelector('input').onchange = e => { sm.scriptRuntime.setVar(meshName, scriptName, ev.name, parseFloat(e.target.value)||0); }; } evBody.appendChild(r); }); chip.appendChild(evBody); } else if (isPlaying && inst?.hasError) { const errBody = document.createElement('div'); errBody.className = 'ev-body'; errBody.innerHTML = `
${esc(inst.errorMsg||'Runtime error')}
`; chip.appendChild(errBody); } parent.appendChild(chip); }); // Add script button const allScripts = ProjectFiles.getAllScriptNames(); const available = allScripts.filter(s => !assigned.includes(s)); if (available.length > 0) { const addBtn = document.createElement('div'); addBtn.className = 'add-script-btn'; addBtn.innerHTML = '+ Assign Script'; // Build dropdown (portaled to body) if (_scriptDropdown) _scriptDropdown.remove(); const dd = document.createElement('div'); dd.className = 'script-dropdown'; document.body.appendChild(dd); _scriptDropdown = dd; available.forEach(sn => { const item = document.createElement('div'); item.className = 'script-dropdown-item'; item.textContent = sn; item.onclick = e => { e.stopPropagation(); const a = { ...State.get('meshScriptAssignments') }; if (!a[meshName]) a[meshName] = []; if (!a[meshName].includes(sn)) a[meshName] = [...a[meshName], sn]; State.set('meshScriptAssignments', a); dd.classList.remove('open'); refresh(); }; dd.appendChild(item); }); addBtn.onclick = e => { e.stopPropagation(); const r = addBtn.getBoundingClientRect(); dd.style.top = r.bottom + 4 + 'px'; dd.style.left = r.left + 'px'; dd.classList.toggle('open'); }; parent.appendChild(addBtn); } if (!isPlaying && assigned.length > 0) { const hint = document.createElement('div'); hint.className = 'play-hint'; hint.textContent = '▶ Press Play to activate scripts and edit exposed vars'; parent.appendChild(hint); } } function promptRename(oldName) { const newName = prompt(`Rename "${oldName}" to:`, oldName); if (newName && newName.trim() && newName.trim() !== oldName) { Engine.getSceneManager()?.renameMesh(oldName, newName.trim()); } } return { refresh }; })(); /* ──────────────────────────────────────────────────────────── §13 UI — HIERARCHY ──────────────────────────────────────────────────────────── */ const UIHierarchy = (() => { const TYPE_ICONS = { Cube:'📦', Box:'📦', Sphere:'🔮', Cylinder:'🥫', Plane:'⬜', Torus:'🍩', Cone:'🔺', Capsule:'💊', Empty:'⦾', Light:'💡', Camera:'🎥' }; function refresh() { const sm = Engine.getSceneManager(); const list = document.getElementById('hierarchy-list'); list.innerHTML = ''; if (!sm) return; const sel = State.get('selectedMeshName'); sm.getMeshNames().forEach(name => { const base = name.replace(/_\d+$/, ''); const icon = TYPE_ICONS[base] || '⬡'; const div = document.createElement('div'); div.className = 'h-item' + (name === sel ? ' selected' : ''); div.dataset.name = name; div.innerHTML = ` ${icon} ${esc(name)} 👁`; div.onclick = () => State.set('selectedMeshName', name); div.ondblclick = () => { // Inline rename const nameSpan = div.querySelector('.h-item-name'); const input = document.createElement('input'); input.className = 'h-item-rename'; input.value = name; nameSpan.replaceWith(input); input.focus(); input.select(); const finish = () => { const v = input.value.trim(); if (v && v !== name) Engine.getSceneManager()?.renameMesh(name, v); else refresh(); }; input.onblur = finish; input.onkeydown = e => { if (e.key==='Enter') input.blur(); if (e.key==='Escape') refresh(); e.stopPropagation(); }; }; div.querySelector('.h-item-vis').onclick = e => { e.stopPropagation(); const m = sm.getMesh(name); if (m) { m.isVisible = !m.isVisible; } }; div.oncontextmenu = e => { e.preventDefault(); State.set('selectedMeshName', name); UIContextMenu.show(e.clientX, e.clientY); }; list.appendChild(div); }); } function refreshSelection() { const sel = State.get('selectedMeshName'); document.querySelectorAll('.h-item').forEach(el => { el.classList.toggle('selected', el.dataset.name === sel); }); } return { refresh, refreshSelection }; })(); /* ──────────────────────────────────────────────────────────── §14 UI — CONSOLE ──────────────────────────────────────────────────────────── */ const UIConsole = (() => { function appendEntry(entry) { const empty = document.getElementById('console-empty'); if (empty) empty.remove(); const filter = State.get('consoleFilter'); if (filter !== 'all' && entry.level !== filter) return; const list = document.getElementById('console-list'); const srcClass = 'src-' + (entry.source || 'unknown').replace(/[^a-zA-Z]/g,''); const div = document.createElement('div'); div.className = `con-entry ${entry.level}`; div.dataset.level = entry.level; div.innerHTML = ` ${entry.timestamp} [${entry.source}] ${esc(entry.message)}`; list.appendChild(div); list.scrollTop = list.scrollHeight; } function clear() { const list = document.getElementById('console-list'); list.innerHTML = '
No output yet.
'; State.get('consoleEntries').length = 0; } function setFilter(level) { State.set('consoleFilter', level); document.querySelectorAll('.cf-btn').forEach(b => b.classList.toggle('active', b.dataset.level === level)); // Rebuild console with filter const entries = State.get('consoleEntries'); const list = document.getElementById('console-list'); list.innerHTML = ''; if (entries.length === 0) { list.innerHTML = '
No output yet.
'; return; } entries.forEach(e => appendEntry(e)); } return { appendEntry, clear, setFilter }; })(); /* ──────────────────────────────────────────────────────────── §15 UI — SCRIPT EDITOR ──────────────────────────────────────────────────────────── */ const UIScriptEditor = (() => { function refreshFileTree() { const list = document.getElementById('script-file-list'); list.innerHTML = ''; const activeFile = State.get('activeScriptFile'); const folder = document.createElement('div'); folder.className = 'sf-folder'; const fh = document.createElement('div'); fh.className = 'sf-folder-header'; fh.innerHTML = `📂Scripts`; const filesDiv = document.createElement('div'); filesDiv.className = 'sf-files open'; fh.onclick = () => { filesDiv.classList.toggle('open'); fh.querySelector('.sf-chevron').classList.toggle('open'); }; ProjectFiles.getAllScriptNames().forEach(name => { const f = document.createElement('div'); f.className = 'sf-file' + (name === activeFile ? ' active' : ''); f.dataset.file = name; f.innerHTML = `📄${esc(name)}`; f.onclick = e => { if (!e.target.dataset.del) openFile(name); }; f.querySelector('.sf-del-btn').onclick = e => { e.stopPropagation(); if (confirm(`Delete "${name}"? This cannot be undone.`)) { ProjectFiles.deleteFile(name); if (State.get('activeScriptFile') === name) closeFile(); refreshFileTree(); } }; filesDiv.appendChild(f); }); folder.appendChild(fh); folder.appendChild(filesDiv); list.appendChild(folder); } function openFile(name) { State.set('activeScriptFile', name); refreshFileTree(); const src = ProjectFiles.getContent(name); document.getElementById('script-empty').style.display = 'none'; document.getElementById('script-editor-open').classList.add('active'); document.getElementById('script-tab-name').textContent = name; if (window.monaco && window._monacoEditor) { const uri = window.monaco.Uri.parse(`file:///scripts/${name}`); let model = window.monaco.editor.getModel(uri); if (!model) { model = window.monaco.editor.createModel(src, 'typescript', uri); model.onDidChangeContent(() => { const v = model.getValue(); ProjectFiles.setContent(name, v); _validateSyntax(name, v); }); } else { if (model.getValue() !== src) model.setValue(src); } window._monacoEditor.setModel(model); window._monacoEditor.layout(); } _validateSyntax(name, src); } function closeFile() { State.set('activeScriptFile', null); document.getElementById('script-empty').style.display = 'flex'; document.getElementById('script-editor-open').classList.remove('active'); } function showNewFileInput() { const wrap = document.getElementById('sf-new-wrap'); wrap.style.display = 'block'; const inp = document.getElementById('sf-new-input'); inp.value = ''; setTimeout(() => inp.focus(), 50); } function _validateSyntax(name, src) { const dot = document.getElementById('script-status-dot'); const bar = document.getElementById('script-status-bar'); try { new Function(stripTypes(src)); dot.className = 'script-dot-sm ok'; bar.className = 'script-status-bar'; bar.textContent = ''; } catch(e) { dot.className = 'script-dot-sm err'; bar.className = 'script-status-bar visible'; bar.innerHTML = `✕ Syntax error:${esc(e.message)}`; } } return { refreshFileTree, openFile, closeFile, showNewFileInput }; })(); /* ──────────────────────────────────────────────────────────── §16 UI — TOOLBAR ──────────────────────────────────────────────────────────── */ const UIToolbar = (() => { function refreshPlayBtn() { const btn = document.getElementById('play-btn'); const icon = document.getElementById('play-icon'); const label = document.getElementById('play-label'); const playing = State.get('isPlaying'); if (playing) { btn.className = 'tb-btn stop-btn'; icon.innerHTML = ''; label.textContent = 'Stop'; } else { btn.className = 'tb-btn play-btn'; icon.innerHTML = ''; label.textContent = 'Play'; } } function setGizmoMode(mode) { ['translate','rotate','scale'].forEach(m => { document.getElementById(`gizmo-${m}`)?.classList.toggle('active', m===mode); }); const badge = document.getElementById('vp-gizmo-badge'); if (badge) badge.textContent = { translate:'Translate', rotate:'Rotate', scale:'Scale' }[mode] || mode; } return { refreshPlayBtn, setGizmoMode }; })(); /* ──────────────────────────────────────────────────────────── §17 UI — NOTIFICATIONS & CONTEXT MENU ──────────────────────────────────────────────────────────── */ const UINotif = { show(msg, type) { const area = document.getElementById('notif-area'); const n = document.createElement('div'); n.className = `notif${type?' '+type:''}`; n.textContent = msg; area.appendChild(n); setTimeout(() => n.remove(), 2900); } }; const UIContextMenu = { show(x, y) { const menu = document.getElementById('context-menu'); menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.classList.add('open'); }, hide() { document.getElementById('context-menu').classList.remove('open'); }, }; /* ──────────────────────────────────────────────────────────── §18 MONACO EDITOR (TypeScript + Visigence types + theme) ──────────────────────────────────────────────────────────── */ const BABYLON_TYPES_DTS = ` declare namespace BABYLON { class Vector3 { x:number; y:number; z:number; constructor(x?:number,y?:number,z?:number); static Zero():Vector3; static One():Vector3; static Up():Vector3; static Forward():Vector3; static Right():Vector3; static Down():Vector3; static Backward():Vector3; static Left():Vector3; static Lerp(a:Vector3,b:Vector3,t:number):Vector3; static Distance(a:Vector3,b:Vector3):number; static Dot(a:Vector3,b:Vector3):number; static Cross(a:Vector3,b:Vector3):Vector3; static Normalize(v:Vector3):Vector3; static TransformCoordinates(v:Vector3,m:Matrix):Vector3; add(o:Vector3):Vector3; addInPlace(o:Vector3):Vector3; subtract(o:Vector3):Vector3; subtractInPlace(o:Vector3):Vector3; scale(s:number):Vector3; scaleInPlace(s:number):Vector3; multiply(o:Vector3):Vector3; multiplyInPlace(o:Vector3):Vector3; length():number; lengthSquared():number; normalize():Vector3; normalizeToNew():Vector3; clone():Vector3; copyFrom(s:Vector3):Vector3; set(x:number,y:number,z:number):Vector3; equals(o:Vector3):boolean; equalsWithEpsilon(o:Vector3,eps?:number):boolean; negate():Vector3; toArray():number[]; } class Vector2 { x:number; y:number; constructor(x?:number,y?:number); static Zero():Vector2; static One():Vector2; add(o:Vector2):Vector2; subtract(o:Vector2):Vector2; scale(s:number):Vector2; length():number; normalize():Vector2; clone():Vector2; set(x:number,y:number):Vector2; } class Vector4 { x:number; y:number; z:number; w:number; constructor(x?:number,y?:number,z?:number,w?:number); } class Color3 { r:number; g:number; b:number; constructor(r?:number,g?:number,b?:number); static Red():Color3; static Green():Color3; static Blue():Color3; static White():Color3; static Black():Color3; static Gray():Color3; static Yellow():Color3; static Magenta():Color3; static Cyan():Color3; static FromHexString(h:string):Color3; static Lerp(a:Color3,b:Color3,t:number):Color3; static FromArray(a:number[]):Color3; clone():Color3; copyFrom(s:Color3):Color3; toHexString():string; equals(o:Color3):boolean; multiply(o:Color3):Color3; scale(s:number):Color3; toLinearSpace():Color3; toGammaSpace():Color3; toArray(a:number[],off?:number):number[]; asArray():number[]; } class Color4 { r:number; g:number; b:number; a:number; constructor(r?:number,g?:number,b?:number,a?:number); clone():Color4; copyFrom(s:Color4):Color4; add(o:Color4):Color4; scale(s:number):Color4; static Lerp(a:Color4,b:Color4,t:number):Color4; } class Quaternion { x:number; y:number; z:number; w:number; constructor(x?:number,y?:number,z?:number,w?:number); static Identity():Quaternion; static RotationAxis(axis:Vector3,angle:number):Quaternion; static FromEulerAngles(x:number,y:number,z:number):Quaternion; static FromEulerVector(v:Vector3):Quaternion; static Slerp(a:Quaternion,b:Quaternion,t:number):Quaternion; static Dot(a:Quaternion,b:Quaternion):number; toEulerAngles():Vector3; toEulerAnglesToRef(v:Vector3):Quaternion; clone():Quaternion; copyFrom(o:Quaternion):Quaternion; multiply(q:Quaternion):Quaternion; multiplyInPlace(q:Quaternion):Quaternion; normalize():Quaternion; invert():Quaternion; length():number; conjugate():Quaternion; } class Matrix { static Identity():Matrix; static Zero():Matrix; static Translation(x:number,y:number,z:number):Matrix; static RotationAxis(axis:Vector3,angle:number):Matrix; static Scaling(x:number,y:number,z:number):Matrix; static LookAtLH(eye:Vector3,target:Vector3,up:Vector3):Matrix; clone():Matrix; multiply(o:Matrix):Matrix; invert():Matrix; decompose(scale?:Vector3,rot?:Quaternion,trans?:Vector3):boolean; getTranslation():Vector3; getRotation():Quaternion; } namespace Scalar { function Lerp(a:number,b:number,t:number):number; function Clamp(v:number,min:number,max:number):number; function Repeat(v:number,len:number):number; function SmoothStep(f:number,t:number,x:number):number; function RandomRange(min:number,max:number):number; function DeltaAngle(current:number,target:number):number; function PingPong(t:number,len:number):number; function MoveTowards(current:number,target:number,maxDelta:number):number; function NormalizeRadians(a:number):number; function TwoPI:number; } class Material { name:string; wireframe:boolean; alpha:number; transparencyMode:number; static PBRMATERIAL_OPAQUE:number; static PBRMATERIAL_ALPHABLEND:number; dispose():void; } class PBRMaterial extends Material { albedoColor:Color3; metallic:number; roughness:number; emissiveColor:Color3; emissiveIntensity:number; unlit:boolean; directIntensity:number; albedoTexture:Texture|null; bumpTexture:Texture|null; metallicTexture:Texture|null; reflectionTexture:CubeTexture|null; ambientColor:Color3; microSurface:number; useAlphaFromAlbedoTexture:boolean; constructor(name:string,scene:Scene); } class StandardMaterial extends Material { diffuseColor:Color3; specularColor:Color3; emissiveColor:Color3; ambientColor:Color3; specularPower:number; diffuseTexture:Texture|null; bumpTexture:Texture|null; constructor(name:string,scene:Scene); } class TransformNode { name:string; id:string; uniqueId:number; position:Vector3; rotation:Vector3; rotationQuaternion:Quaternion|null; scaling:Vector3; parent:TransformNode|null; metadata:Record; getAbsolutePosition():Vector3; setAbsolutePosition(p:Vector3):void; rotate(axis:Vector3,angle:number,space?:number):void; translate(axis:Vector3,dist:number,space?:number):void; lookAt(target:Vector3,yaw?:number,pitch?:number,roll?:number):void; getScene():Scene; getWorldMatrix():Matrix; computeWorldMatrix(force?:boolean):Matrix; dispose():void; } class AbstractMesh extends TransformNode { material:Material|null; isVisible:boolean; isPickable:boolean; receiveShadows:boolean; getBoundingInfo():BoundingInfo; getVerticesData(kind:string):any; getTotalVertices():number; getTotalIndices():number; clone(name:string,parent:AbstractMesh|null):AbstractMesh; intersectsMesh(mesh:AbstractMesh,precise?:boolean):boolean; intersectsPoint(p:Vector3):boolean; showBoundingBox:boolean; } class Mesh extends AbstractMesh { static FRONTSIDE:number; static BACKSIDE:number; static DOUBLESIDE:number; setVerticesData(kind:string,data:any,updatable?:boolean):void; updateVerticesData(kind:string,data:any):void; createInstance(name:string):InstancedMesh; applyDisplacementMap(url:string,minH:number,maxH:number):void; convertToFlatShadedMesh():void; convertToUnIndexedMesh():void; bakeTransformIntoVertices(transform:Matrix):void; } class InstancedMesh extends AbstractMesh {} class GroundMesh extends Mesh { getHeightAtCoordinates(x:number,z:number):number; } namespace MeshBuilder { function CreateBox(name:string,o:{size?:number,width?:number,height?:number,depth?:number,faceColors?:Color4[],updatable?:boolean},scene?:Scene):Mesh; function CreateSphere(name:string,o:{diameter?:number,segments?:number,updatable?:boolean},scene?:Scene):Mesh; function CreateCylinder(name:string,o:{height?:number,diameter?:number,diameterTop?:number,diameterBottom?:number,tessellation?:number,subdivisions?:number,updatable?:boolean},scene?:Scene):Mesh; function CreatePlane(name:string,o:{size?:number,width?:number,height?:number,sideOrientation?:number,updatable?:boolean},scene?:Scene):Mesh; function CreateGround(name:string,o:{width?:number,height?:number,subdivisions?:number,updatable?:boolean},scene?:Scene):GroundMesh; function CreateTorus(name:string,o:{diameter?:number,thickness?:number,tessellation?:number,updatable?:boolean},scene?:Scene):Mesh; function CreateTorusKnot(name:string,o:{radius?:number,tube?:number,radialSegments?:number,tubularSegments?:number,p?:number,q?:number},scene?:Scene):Mesh; function CreateCapsule(name:string,o:{height?:number,radius?:number,tessellation?:number,subdivisions?:number},scene?:Scene):Mesh; function CreateLines(name:string,o:{points:Vector3[],updatable?:boolean},scene?:Scene):Mesh; function CreateDashedLines(name:string,o:{points:Vector3[],dashSize?:number,gapSize?:number,dashNb?:number},scene?:Scene):Mesh; function CreateRibbon(name:string,o:{pathArray:Vector3[][],closeArray?:boolean,closePath?:boolean},scene?:Scene):Mesh; function CreateDisc(name:string,o:{radius?:number,tessellation?:number},scene?:Scene):Mesh; function CreateIcoSphere(name:string,o:{radius?:number,subdivisions?:number,flat?:boolean},scene?:Scene):Mesh; function CreateDecal(name:string,sourceMesh:AbstractMesh,o:{position:Vector3,normal:Vector3,size:Vector3}):Mesh; function CreateTube(name:string,o:{path:Vector3[],radius?:number,tessellation?:number,updatable?:boolean},scene?:Scene):Mesh; function CreatePolyhedron(name:string,o:{type?:number,size?:number,custom?:any},scene?:Scene):Mesh; } namespace VertexBuffer { const PositionKind:string; const NormalKind:string; const UVKind:string; const ColorKind:string; const TangentKind:string; } class BoundingInfo { minimum:Vector3; maximum:Vector3; boundingBox:BoundingBox; boundingSphere:BoundingSphere; } class BoundingBox { minimumWorld:Vector3; maximumWorld:Vector3; centerWorld:Vector3; } class BoundingSphere { centerWorld:Vector3; radiusWorld:number; } class Light { name:string; intensity:number; diffuse:Color3; specular:Color3; range:number; } class DirectionalLight extends Light { position:Vector3; direction:Vector3; constructor(n:string,d:Vector3,s:Scene); } class PointLight extends Light { position:Vector3; constructor(n:string,p:Vector3,s:Scene); radius:number; } class SpotLight extends Light { position:Vector3; direction:Vector3; angle:number; exponent:number; constructor(n:string,p:Vector3,d:Vector3,angle:number,exp:number,s:Scene); } class HemisphericLight extends Light { direction:Vector3; groundColor:Color3; constructor(n:string,d:Vector3,s:Scene); } class Camera extends TransformNode { fov:number; minZ:number; maxZ:number; getDirection(v:Vector3):Vector3; } class ArcRotateCamera extends Camera { alpha:number; beta:number; radius:number; target:Vector3; wheelPrecision:number; lowerRadiusLimit:number; upperRadiusLimit:number; lowerAlphaLimit:number; upperAlphaLimit:number; lowerBetaLimit:number; upperBetaLimit:number; setTarget(t:Vector3):void; attachControl(canvas:HTMLElement,noPreventDefault?:boolean):void; constructor(n:string,alpha:number,beta:number,radius:number,target:Vector3,scene:Scene); } class FreeCamera extends Camera { position:Vector3; rotation:Vector3; speed:number; inputs:any; attachControl(canvas:HTMLElement,noPreventDefault?:boolean):void; constructor(n:string,pos:Vector3,scene:Scene); } class ShadowGenerator { bias:number; blurKernel:number; useBlurExponentialShadowMap:boolean; usePoissonSampling:boolean; useExponentialShadowMap:boolean; addShadowCaster(mesh:AbstractMesh,includeDescendants?:boolean):void; removeShadowCaster(mesh:AbstractMesh):void; constructor(size:number,light:IShadowLight); } interface IShadowLight extends Light {} class Texture { constructor(url:string,scene:Scene,noMipmap?:boolean,invertY?:boolean); uScale:number; vScale:number; uOffset:number; vOffset:number; wrapU:number; wrapV:number; static WRAP_ADDRESSMODE:number; static CLAMP_ADDRESSMODE:number; static MIRROR_ADDRESSMODE:number; dispose():void; } class DynamicTexture extends Texture { constructor(name:string,options:{width:number,height:number},scene:Scene,generateMipMaps?:boolean); getContext():CanvasRenderingContext2D; update(invertY?:boolean):void; drawText(text:string,x:number,y:number,font:string,color:string,clearColor:string,invertY?:boolean,update?:boolean):void; } class CubeTexture extends Texture { constructor(url:string,scene:Scene); static CreateFromPrefilteredData(url:string,scene:Scene):CubeTexture; } class VideoTexture extends Texture { constructor(name:string,src:string|string[],scene:Scene,generateMipMaps?:boolean,invertY?:boolean); video:HTMLVideoElement; } class RenderTargetTexture extends Texture { constructor(name:string,size:number,scene:Scene,generateMipMaps?:boolean); renderList:AbstractMesh[]; onBeforeRenderObservable:Observable; onAfterRenderObservable:Observable; } class Observable { add(callback:(data:T,state:any)=>void):Observer; remove(observer:Observer):void; notifyObservers(data:T):void; hasObservers():boolean; } class Observer {} class Scene { activeCamera:Camera; meshes:AbstractMesh[]; lights:Light[]; cameras:Camera[]; materials:Material[]; textures:Texture[]; metadata:Record; onBeforeRenderObservable:Observable; onAfterRenderObservable:Observable; onPointerObservable:Observable; onKeyboardObservable:Observable; animationGroups:AnimationGroup[]; registerBeforeRender(fn:()=>void):void; registerAfterRender(fn:()=>void):void; unregisterBeforeRender(fn:()=>void):void; unregisterAfterRender(fn:()=>void):void; getMeshByName(name:string):AbstractMesh|null; getMeshById(id:string):AbstractMesh|null; getMeshByUniqueId(id:number):AbstractMesh|null; getLightByName(name:string):Light|null; getMaterialByName(name:string):Material|null; pick(x:number,y:number):PickingInfo; pickWithRay(ray:Ray):PickingInfo; dispose():void; beginAnimation(target:any,from:number,to:number,loop?:boolean,speedRatio?:number):Animatable; stopAnimation(target:any):void; clearColor:Color4; ambientColor:Color3; fogMode:number; fogColor:Color3; fogDensity:number; gravity:Vector3; enablePhysics(gravity:Vector3,plugin:any):boolean; static FOGMODE_NONE:number; static FOGMODE_EXP:number; static FOGMODE_EXP2:number; static FOGMODE_LINEAR:number; } class PickingInfo { hit:boolean; distance:number; pickedMesh:AbstractMesh|null; pickedPoint:Vector3|null; getNormal(useWorldCoordinates?:boolean):Vector3|null; getTextureCoordinates():Vector2|null; bu:number; bv:number; faceId:number; } class PointerInfo { type:number; event:PointerEvent; pickInfo:PickingInfo|null; } class KeyboardInfo { type:number; event:KeyboardEvent; } enum PointerEventTypes { POINTERDOWN=1, POINTERUP=2, POINTERMOVE=4, POINTERWHEEL=8, POINTERPICK=16, POINTERTAP=32, POINTERDOUBLETAP=64 } class Ray { origin:Vector3; direction:Vector3; length:number; constructor(origin:Vector3,direction:Vector3,length?:number); intersectsMesh(mesh:AbstractMesh,fastCheck?:boolean):PickingInfo; static CreateNew(x:number,y:number,viewportWidth:number,viewportHeight:number,world:Matrix,view:Matrix,projection:Matrix):Ray; static CreateFromPickingInfo(info:PickingInfo):Ray; } class Animation { static CreateAndStartAnimation(name:string,mesh:TransformNode,targetProperty:string,fps:number,totalFrame:number,from:any,to:any,loopMode?:number,easingFunction?:EasingFunction,onAnimationEnd?:()=>void):Animatable; static ANIMATIONTYPE_FLOAT:number; static ANIMATIONTYPE_VECTOR3:number; static ANIMATIONTYPE_QUATERNION:number; static ANIMATIONTYPE_COLOR3:number; static ANIMATIONLOOPMODE_CYCLE:number; static ANIMATIONLOOPMODE_CONSTANT:number; static ANIMATIONLOOPMODE_RELATIVE:number; } class Animatable { pause():void; restart():void; stop():void; } class AnimationGroup { name:string; play(loop?:boolean):void; stop():void; pause():void; reset():void; } class EasingFunction {} class BackEase extends EasingFunction {} class BounceEase extends EasingFunction {} class ElasticEase extends EasingFunction {} class SineEase extends EasingFunction {} class QuadraticEase extends EasingFunction {} class QuarticEase extends EasingFunction {} class ExponentialEase extends EasingFunction {} class PowerEase extends EasingFunction {} class BezierCurveEase extends EasingFunction {} class CubicEase extends EasingFunction {} class CircleEase extends EasingFunction {} class GizmoManager { positionGizmoEnabled:boolean; rotationGizmoEnabled:boolean; scaleGizmoEnabled:boolean; usePointerToAttachGizmos:boolean; gizmos:{positionGizmo:PositionGizmo|null,rotationGizmo:RotationGizmo|null,scaleGizmo:ScaleGizmo|null}; constructor(scene:Scene); attachToMesh(mesh:AbstractMesh|null):void; dispose():void; } class PositionGizmo { dragBehavior:any; xGizmo:AxisDragGizmo; yGizmo:AxisDragGizmo; zGizmo:AxisDragGizmo; } class RotationGizmo { xGizmo:AxisScaleGizmo; yGizmo:AxisScaleGizmo; zGizmo:AxisScaleGizmo; } class ScaleGizmo {} class AxisDragGizmo {} class AxisScaleGizmo {} class DefaultRenderingPipeline { samples:number; bloomEnabled:boolean; bloomThreshold:number; bloomWeight:number; bloomKernel:number; imageProcessingEnabled:boolean; imageProcessing:ImageProcessingConfiguration; depthOfFieldEnabled:boolean; depthOfField:DepthOfFieldEffect; chromaticAberrationEnabled:boolean; grainEnabled:boolean; constructor(name:string,hdr:boolean,scene:Scene,cameras?:Camera[]); } class ImageProcessingConfiguration { contrast:number; exposure:number; vignetteEnabled:boolean; vignetteWeight:number; vignetteColor:Color4; vignetteBlendMode:number; colorGradingEnabled:boolean; colorCurvesEnabled:boolean; toneMappingEnabled:boolean; toneMappingType:number; isEnabled:boolean; } class DepthOfFieldEffect { focusDistance:number; focalLength:number; fStop:number; } namespace Tools { function ToRadians(d:number):number; function ToDegrees(r:number):number; function Log(m:string):void; function Warn(m:string):void; function Error(m:string):void; function CreateScreenshot(engine:Engine,camera:Camera,size:any,successCallback?:(data:string)=>void):void; function LoadFile(url:string,onSuccess:(data:string|ArrayBuffer)=>void,onProgress?:(event:ProgressEvent)=>void,offlineProvider?:any,useArrayBuffer?:boolean,onError?:(request:XMLHttpRequest|undefined,exception:any)=>void):void; } class Engine { static Version:string; getFps():number; resize():void; getRenderingCanvas():HTMLCanvasElement|null; dispose():void; runRenderLoop(fn:()=>void):void; stopRenderLoop(fn?:()=>void):void; } class NullEngine extends Engine {} namespace ParticleSystem { class ParticleSystem { constructor(name:string,capacity:number,scene:Scene); emitter:AbstractMesh|Vector3; minEmitPower:number; maxEmitPower:number; minLifeTime:number; maxLifeTime:number; emitRate:number; minSize:number; maxSize:number; color1:Color4; color2:Color4; colorDead:Color4; updateSpeed:number; gravity:Vector3; start():void; stop():void; dispose():void; } } } declare const BABYLON: typeof BABYLON; declare const scene: BABYLON.Scene; declare const console: { log(...args:any[]):void; warn(...args:any[]):void; error(...args:any[]):void; info(...args:any[]):void; debug(...args:any[]):void; }; `; function setupMonaco() { window.MonacoEnvironment = { getWorkerUrl(_, label) { const base = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/'; const worker = (label==='typescript'||label==='javascript') ? 'vs/language/typescript/ts.worker.js' : 'vs/base/worker/workerMain.js'; return `data:text/javascript;charset=utf-8,${encodeURIComponent(`self.MonacoEnvironment={baseUrl:'${base}'};importScripts('${base}${worker}');`)}`; } }; require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs' } }); require(['vs/editor/editor.main'], function(monaco) { window.monaco = monaco; monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES2020, module: monaco.languages.typescript.ModuleKind.None, lib: ['ES2020'], noImplicitAny: false, strict: false, noLib: true, allowNonTsExtensions: true, experimentalDecorators: true, }); monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false, diagnosticCodesToIgnore: [2304, 2580, 2307, 7006, 2792, 2339, 2345, 2341, 1238, 2322, 2529], }); // Add BABYLON type definitions monaco.languages.typescript.typescriptDefaults.addExtraLib(BABYLON_TYPES_DTS, 'ts:babylon.d.ts'); // Add standard globals monaco.languages.typescript.typescriptDefaults.addExtraLib(` declare var Math: { PI:number; E:number; LN2:number; LN10:number; LOG2E:number; abs(x:number):number; ceil(x:number):number; floor(x:number):number; round(x:number):number; max(...v:number[]):number; min(...v:number[]):number; pow(x:number,y:number):number; sqrt(x:number):number; cbrt(x:number):number; sin(x:number):number; cos(x:number):number; tan(x:number):number; asin(x:number):number; acos(x:number):number; atan(x:number):number; atan2(y:number,x:number):number; sinh(x:number):number; cosh(x:number):number; random():number; sign(x:number):number; log(x:number):number; log2(x:number):number; exp(x:number):number; expm1(x:number):number; log1p(x:number):number; trunc(x:number):number; hypot(...v:number[]):number; clz32(x:number):number; fround(x:number):number; imul(x:number,y:number):number; }; declare function parseInt(s:string,radix?:number):number; declare function parseFloat(s:string):number; declare function isNaN(n:number):boolean; declare function isFinite(n:number):boolean; declare function encodeURIComponent(s:string):string; declare function decodeURIComponent(s:string):string; declare function JSON_stringify_only(value:any,replacer?:any,space?:any):string; declare function setTimeout(h:(...a:any[])=>void,t?:number,...a:any[]):number; declare function clearTimeout(id?:number):void; declare function setInterval(h:(...a:any[])=>void,t?:number,...a:any[]):number; declare function clearInterval(id?:number):void; declare function requestAnimationFrame(cb:(t:number)=>void):number; declare function cancelAnimationFrame(id:number):void; declare class Map{ constructor(e?:[K,V][]); set(k:K,v:V):this; get(k:K):V|undefined; has(k:K):boolean; delete(k:K):boolean; clear():void; forEach(cb:(v:V,k:K,m:Map)=>void):void; keys():IterableIterator; values():IterableIterator; entries():IterableIterator<[K,V]>; size:number; } declare class Set{ constructor(v?:T[]); add(v:T):this; has(v:T):boolean; delete(v:T):boolean; clear():void; size:number; forEach(cb:(v:T,k:T,s:Set)=>void):void; } declare class WeakMap{ set(k:K,v:V):this; get(k:K):V|undefined; has(k:K):boolean; delete(k:K):boolean; } declare class WeakSet{ add(v:T):this; has(v:T):boolean; delete(v:T):boolean; } declare const performance:{ now():number; }; declare const window:{ innerWidth:number; innerHeight:number; }; `, 'ts:globals.d.ts'); // Visigence dark theme monaco.editor.defineTheme('visigence', { base: 'vs-dark', inherit: true, rules: [ { token:'', foreground:'c8d4e0' }, { token:'comment', foreground:'2e4060', fontStyle:'italic' }, { token:'keyword', foreground:'6cb6ff' }, { token:'keyword.control', foreground:'6cb6ff' }, { token:'keyword.operator', foreground:'4a9fe0' }, { token:'string', foreground:'7ec799' }, { token:'string.escape', foreground:'55b08e' }, { token:'number', foreground:'d4a96a' }, { token:'number.hex', foreground:'d4a96a' }, { token:'type', foreground:'e5c17b' }, { token:'type.identifier', foreground:'e5c17b' }, { token:'class-name', foreground:'e8d07a' }, { token:'identifier', foreground:'c8d4e0' }, { token:'function', foreground:'79b8ff' }, { token:'method', foreground:'79b8ff' }, { token:'variable', foreground:'c8d4e0' }, { token:'variable.parameter', foreground:'b0b8c0' }, { token:'delimiter', foreground:'3a4a60' }, { token:'delimiter.bracket', foreground:'4a6080' }, { token:'delimiter.curly', foreground:'4a6080' }, { token:'delimiter.paren', foreground:'4a6080' }, { token:'decorator', foreground:'1a7fff', fontStyle:'italic' }, { token:'annotation', foreground:'1a7fff', fontStyle:'italic' }, { token:'regexp', foreground:'7ec799' }, { token:'invalid', foreground:'ff4d6a', background:'300010' }, { token:'tag', foreground:'6cb6ff' }, { token:'attribute.name', foreground:'79b8ff' }, { token:'attribute.value', foreground:'7ec799' }, ], colors: { 'editor.background': '#030b16', 'editor.foreground': '#c8d4e0', 'editor.lineHighlightBackground': '#081828', 'editor.lineHighlightBorder': '#081828', 'editor.selectionBackground': '#1a7fff22', 'editor.inactiveSelectionBackground': '#1a7fff10', 'editor.selectionHighlightBackground': '#1a7fff18', 'editor.wordHighlightBackground': '#1a7fff14', 'editor.wordHighlightStrongBackground': '#1a7fff20', 'editor.findMatchBackground': '#1a7fff30', 'editor.findMatchHighlightBackground': '#1a7fff18', 'editorCursor.foreground': '#1a7fff', 'editorCursor.background': '#030b16', 'editorLineNumber.foreground': '#1a2c40', 'editorLineNumber.activeForeground': '#2a4060', 'editorBracketMatch.background': '#1a7fff18', 'editorBracketMatch.border': '#1a7fff45', 'editorBracketHighlight.foreground1': '#1a7fff', 'editorBracketHighlight.foreground2': '#00d4ff', 'editorBracketHighlight.foreground3': '#7ec799', 'editorGutter.background': '#030b16', 'editorWidget.background': '#061018', 'editorWidget.border': '#1a3050', 'editorSuggestWidget.background': '#061018', 'editorSuggestWidget.border': '#1a3050', 'editorSuggestWidget.foreground': '#c8d4e0', 'editorSuggestWidget.highlightForeground':'#4da3ff', 'editorSuggestWidget.selectedBackground':'#1a7fff18', 'editorSuggestWidget.selectedForeground':'#e8eef4', 'editorHoverWidget.background': '#061018', 'editorHoverWidget.border': '#1a3050', 'editorHoverWidget.foreground': '#c8d4e0', 'editorMarkerNavigation.background': '#061018', 'editorRuler.foreground': '#0d2040', 'editorIndentGuide.background1': '#0c1e30', 'editorIndentGuide.activeBackground1': '#1a3050', 'editorCodeLens.foreground': '#2a4060', 'editorInfo.foreground': '#4da3ff', 'editorWarning.foreground': '#ffb84d', 'editorError.foreground': '#ff4d6a', 'scrollbarSlider.background': '#1a7fff14', 'scrollbarSlider.hoverBackground': '#1a7fff28', 'scrollbarSlider.activeBackground': '#1a7fff40', 'minimap.background': '#030b16', 'peekView.border': '#1a7fff', 'peekViewEditor.background': '#040e1a', 'peekViewResult.background': '#061018', 'peekViewResult.selectionBackground': '#1a7fff20', 'dropdown.background': '#061018', 'dropdown.border': '#1a3050', 'dropdown.foreground': '#c8d4e0', 'input.background': '#061018', 'input.border': '#1a3050', 'input.foreground': '#c8d4e0', 'input.placeholderForeground': '#2a4060', 'focusBorder': '#1a7fff50', 'contrastBorder': '#1a7fff30', 'list.highlightForeground': '#4da3ff', 'list.focusBackground': '#1a7fff18', 'list.hoverBackground': '#0a1828', 'list.activeSelectionBackground': '#1a7fff20', 'list.activeSelectionForeground': '#e8eef4', 'editorGroupHeader.tabsBackground': '#020810', 'tab.activeBackground': '#040e1a', 'tab.activeForeground': '#c8d4e0', 'tab.inactiveBackground': '#020810', 'tab.inactiveForeground': '#2a4060', 'tab.border': '#0a1828', 'tab.activeBorderTop': '#1a7fff', 'statusBar.background': '#0a1828', 'statusBar.foreground': '#4a6080', 'statusBar.noFolderBackground': '#0a1828', 'terminal.background': '#030b16', 'terminal.foreground': '#c8d4e0', 'sideBar.background': '#040e1a', 'sideBarSectionHeader.background': '#061018', 'panel.background': '#030b16', 'panel.border': '#0d2040', 'activityBar.background': '#020810', 'activityBar.foreground': '#4a6080', } }); // Create editor const editor = monaco.editor.create(document.getElementById('monaco-container'), { language: 'typescript', theme: 'visigence', value: '', fontSize: 12.5, fontFamily: "'JetBrains Mono','Fira Code','Cascadia Code','Consolas',Menlo,monospace", fontLigatures: true, lineHeight: 20, letterSpacing: 0.3, minimap: { enabled: false }, lineNumbers: 'on', lineNumbersMinChars: 3, scrollBeyondLastLine: false, automaticLayout: true, tabSize: 2, insertSpaces: true, wordWrap: 'off', padding: { top: 14, bottom: 14 }, renderLineHighlight: 'gutter', bracketPairColorization: { enabled: true }, guides: { bracketPairs: true, indentation: true }, smoothScrolling: true, cursorBlinking: 'smooth', cursorSmoothCaretAnimation: 'on', cursorWidth: 2, formatOnPaste: true, formatOnType: false, quickSuggestions: { other: true, comments: false, strings: false }, suggestOnTriggerCharacters: true, acceptSuggestionOnEnter: 'smart', tabCompletion: 'on', snippetSuggestions: 'inline', parameterHints: { enabled: true, cycle: true }, hover: { enabled: true, delay: 400 }, occurrencesHighlight: 'singleFile', inlayHints: { enabled: 'off' }, scrollbar: { verticalScrollbarSize: 5, horizontalScrollbarSize: 5, useShadows: false, alwaysConsumeMouseWheel: false }, overviewRulerBorder: false, overviewRulerLanes: 0, glyphMargin: false, codeLens: false, contextmenu: true, renderWhitespace: 'none', renderControlCharacters: false, lineDecorationsWidth: 4, foldingHighlight: false, links: true, mouseWheelZoom: false, multiCursorMergeOverlapping: true, dragAndDrop: true, accessibilitySupport: 'off', stickyScroll: { enabled: false }, 'semanticHighlighting.enabled': true, }); window._monacoEditor = editor; window._monacoEditorOptions = { fontSize: 12.5, minimap: false, wordWrap: false, ligatures: true, }; // Auto-open first file const files = ProjectFiles.getAllScriptNames(); if (files.length > 0) UIScriptEditor.openFile(files[0]); logToConsole('info', 'Monaco TypeScript IDE ready — BabylonJS type definitions loaded', 'Engine'); }); } /* ──────────────────────────────────────────────────────────── §19 EVENT WIRING ──────────────────────────────────────────────────────────── */ function wireEvents() { /* Play / Stop */ document.getElementById('play-btn').onclick = () => State.set('isPlaying', !State.get('isPlaying')); /* Gizmo buttons */ ['translate','rotate','scale'].forEach(m => { document.getElementById(`gizmo-${m}`).onclick = () => { State.set('gizmoMode', m); UIToolbar.setGizmoMode(m); }; }); /* Focus */ document.getElementById('focus-btn').onclick = () => Engine.getSceneManager()?.focusOnSelected(); /* Render modes */ const rmMap = { default:'Default', wire:'Wireframe', unlit:'Unlit' }; ['default','wire','unlit'].forEach(id => { document.getElementById(`rm-${id}`).onclick = () => { const mode = rmMap[id]; State.set('renderMode', mode); ['default','wire','unlit'].forEach(i2 => document.getElementById(`rm-${i2}`).classList.toggle('active', i2===id)); }; }); /* Grid toggle */ document.getElementById('grid-btn').onclick = function() { this.classList.toggle('active'); const v = this.classList.contains('active'); State.set('gridVisible', v); Engine.getSceneManager()?.setGridVisible(v); }; /* Shadows toggle */ document.getElementById('shadows-btn').onclick = function() { this.classList.toggle('active'); const v = this.classList.contains('active'); State.set('shadowsEnabled', v); Engine.getSceneManager()?.setSoftShadows(v); }; /* Bloom toggle */ document.getElementById('bloom-btn').onclick = function() { this.classList.toggle('active'); const v = this.classList.contains('active'); Engine.getSceneManager()?.setPPBloom(v); }; /* Undo / Redo buttons */ document.getElementById('undo-btn').onclick = () => UndoRedo.undo(); document.getElementById('redo-btn').onclick = () => UndoRedo.redo(); /* Exports */ document.getElementById('export-html-btn').onclick = () => ExportManager.exportHTML(); document.getElementById('export-json-btn').onclick = () => ExportManager.exportJSON(); document.getElementById('export-zip-btn').onclick = () => ExportManager.exportSourceZip(); /* Inspector lock */ document.getElementById('insp-lock').onclick = function() { const locked = State.get('inspectorLocked'); State.set('inspectorLocked', !locked); this.style.color = locked ? '' : 'var(--blue-bright)'; }; /* Hierarchy panel actions */ document.getElementById('h-del').onclick = () => { const s=State.get('selectedMeshName'); Engine.getSceneManager()?.removeMesh(s); }; document.getElementById('h-dup').onclick = () => Engine.getSceneManager()?.duplicateMesh(State.get('selectedMeshName')); document.getElementById('h-focus').onclick = () => Engine.getSceneManager()?.focusOnSelected(); /* Add object button & menu */ const addBtn = document.getElementById('add-object-btn'); const addMenu = document.getElementById('add-object-menu'); addBtn.onclick = e => { e.stopPropagation(); const r = addBtn.getBoundingClientRect(); addMenu.style.cssText = `position:fixed;top:${r.bottom+4}px;left:${r.left}px;`; addMenu.classList.toggle('open'); }; addMenu.querySelectorAll('.ao-item').forEach(item => { item.onclick = () => { Engine.getSceneManager()?.addMesh(item.dataset.type); addMenu.classList.remove('open'); }; }); /* Console */ document.getElementById('console-clear').onclick = () => UIConsole.clear(); document.querySelectorAll('.cf-btn').forEach(btn => { btn.onclick = () => UIConsole.setFilter(btn.dataset.level); }); /* Console resize */ const resizeHandle = document.getElementById('console-resize'); const consolePanel = document.getElementById('console-panel'); let _resizing = false, _startY, _startH; resizeHandle.addEventListener('mousedown', e => { _resizing=true; _startY=e.clientY; _startH=consolePanel.offsetHeight; resizeHandle.classList.add('dragging'); document.addEventListener('mousemove', onRsMove); document.addEventListener('mouseup', onRsUp); }); function onRsMove(e) { if (!_resizing) return; const h = Math.max(48, Math.min(700, _startH + (_startY - e.clientY))); consolePanel.style.height = h + 'px'; } function onRsUp() { _resizing = false; resizeHandle.classList.remove('dragging'); document.removeEventListener('mousemove', onRsMove); document.removeEventListener('mouseup', onRsUp); } /* Tabs */ document.querySelectorAll('.mb-tab').forEach(tab => { tab.onclick = () => { const t = tab.dataset.tab; document.querySelectorAll('.mb-tab').forEach(b => b.classList.toggle('active', b===tab)); document.getElementById('scene-view').style.display = t==='scene' ? 'flex' : 'none'; document.getElementById('script-view').style.display = t==='scripts' ? 'flex' : 'none'; document.getElementById('settings-view').style.display= t==='settings' ? 'block' : 'none'; State.set('activeTab', t); if (window._monacoEditor) setTimeout(() => window._monacoEditor.layout(), 50); }; }); /* Script sidebar */ document.getElementById('new-file-btn').onclick = () => UIScriptEditor.showNewFileInput(); document.getElementById('empty-new-btn').onclick = () => { document.querySelectorAll('.mb-tab').forEach(t => { if (t.dataset.tab==='scripts') t.click(); }); UIScriptEditor.showNewFileInput(); }; const sfNewInput = document.getElementById('sf-new-input'); const sfNewWrap = document.getElementById('sf-new-wrap'); sfNewInput.onkeydown = e => { if (e.key === 'Enter') { let name = sfNewInput.value.trim().replace(/\.ts$/, ''); if (name) { name = name.replace(/[^a-zA-Z0-9_$]/g, '_') + '.ts'; ProjectFiles.addFile(name); UIScriptEditor.refreshFileTree(); UIScriptEditor.openFile(name); sfNewWrap.style.display = 'none'; } } if (e.key === 'Escape') { sfNewWrap.style.display = 'none'; } e.stopPropagation(); }; /* Run script button */ document.getElementById('run-script-btn').onclick = () => { const name = State.get('activeScriptFile'); if (!name) return; const src = ProjectFiles.getContent(name); const sm = Engine.getSceneManager(); const rt = sm ? sm.scriptRuntime : new ScriptRuntime(); if (sm) rt.setScene(sm.scene); rt.runStandalone(name, src); }; /* Context menu actions */ document.getElementById('context-menu').addEventListener('click', e => { const item = e.target.closest('.cm-item'); if (!item) return; const sm = Engine.getSceneManager(); const sel = State.get('selectedMeshName'); const action = item.dataset.action; UIContextMenu.hide(); switch(action) { case 'focus': sm?.focusOnSelected(); break; case 'rename': { const n = prompt(`Rename "${sel}":`, sel); if (n?.trim() && n.trim()!==sel) sm?.renameMesh(sel, n.trim()); break; } case 'duplicate': sm?.duplicateMesh(sel); break; case 'toggle-vis': { const m=sm?.getMesh(sel); if(m) m.isVisible=!m.isVisible; } break; case 'delete': sm?.removeMesh(sel); break; } }); /* Close context menu on outside click */ document.addEventListener('click', e => { if (!e.target.closest('#context-menu')) UIContextMenu.hide(); if (!e.target.closest('#add-object-menu') && !e.target.closest('#add-object-btn')) document.getElementById('add-object-menu').classList.remove('open'); }); /* Settings panel */ _wireSettings(); } function _wireSettings() { const sm = () => Engine.getSceneManager(); // Toggle switches document.querySelectorAll('.toggle-switch[data-setting]').forEach(sw => { sw.onclick = function() { this.classList.toggle('on'); const on = this.classList.contains('on'); const setting = this.dataset.setting; switch(setting) { case 'bloom': sm()?.setPPBloom(on); break; case 'vignette': sm()?.setPPVignette(on); break; case 'softShadows': sm()?.setSoftShadows(on); break; } }; }); // Monaco editor settings document.getElementById('s-font-size').onchange = e => { if (window._monacoEditor) window._monacoEditor.updateOptions({ fontSize: parseInt(e.target.value) }); }; document.getElementById('s-minimap').onclick = function() { this.classList.toggle('on'); if (window._monacoEditor) window._monacoEditor.updateOptions({ minimap:{ enabled: this.classList.contains('on') } }); }; document.getElementById('s-wordwrap').onclick = function() { this.classList.toggle('on'); if (window._monacoEditor) window._monacoEditor.updateOptions({ wordWrap: this.classList.contains('on') ? 'on' : 'off' }); }; document.getElementById('s-ligatures').onclick = function() { this.classList.toggle('on'); if (window._monacoEditor) window._monacoEditor.updateOptions({ fontLigatures: this.classList.contains('on') }); }; // Sliders — wire with label updates [ ['s-bloom-threshold', 's-bloom-threshold-val', 2, v => sm()?.setPPBloomThreshold(v)], ['s-bloom-weight', 's-bloom-weight-val', 2, v => sm()?.setPPBloomWeight(v)], ['s-vignette-weight', 's-vignette-weight-val', 2, v => sm()?.setPPVignetteWeight(v)], ['s-contrast', 's-contrast-val', 2, v => sm()?.setPPContrast(v)], ['s-exposure', 's-exposure-val', 2, v => sm()?.setPPExposure(v)], ['s-sun-intensity', 's-sun-intensity-val', 2, v => sm()?.setSunIntensity(v)], ['s-ambient-intensity','s-ambient-intensity-val',2, v => sm()?.setAmbientIntensity(v)], ['s-fov', 's-fov-val', 0, v => { sm()?.setCameraFOV(v); document.getElementById('s-fov-val').textContent=v+'°'; return true; }], ['s-wheel-prec', 's-wheel-prec-val', 0, v => sm()?.setWheelPrecision(v)], ].forEach(([sliderId, valId, decimals, fn]) => { const slider = document.getElementById(sliderId); const valEl = document.getElementById(valId); if (!slider) return; slider.addEventListener('input', () => { const v = parseFloat(slider.value); const result = fn(v); if (!result) valEl.textContent = v.toFixed(decimals); }); }); } /* ──────────────────────────────────────────────────────────── §20 KEYBOARD SHORTCUTS ──────────────────────────────────────────────────────────── */ function setupKeyboard() { window.addEventListener('keydown', e => { const tag = e.target.tagName; const isInput = tag==='INPUT'||tag==='TEXTAREA'||e.target.isContentEditable; const inMonaco = !!e.target.closest?.('.monaco-editor'); if (isInput || inMonaco) return; const sm = Engine.getSceneManager(); const sel = State.get('selectedMeshName'); const key = e.key.toLowerCase(); // Ctrl/Cmd combos if (e.ctrlKey || e.metaKey) { switch(key) { case 'z': e.preventDefault(); UndoRedo.undo(); return; case 'y': e.preventDefault(); UndoRedo.redo(); return; case 'd': e.preventDefault(); sm?.duplicateMesh(sel); return; case 'a': { // Select all — cycle through meshes e.preventDefault(); const names = sm?.getMeshNames() || []; if (names.length) State.set('selectedMeshName', names[0]); return; } } } switch(key) { case 'w': State.set('gizmoMode', 'translate'); UIToolbar.setGizmoMode('translate'); break; case 'e': State.set('gizmoMode', 'rotate'); UIToolbar.setGizmoMode('rotate'); break; case 'r': State.set('gizmoMode', 'scale'); UIToolbar.setGizmoMode('scale'); break; case 'f': sm?.focusOnSelected(); break; case ' ': e.preventDefault(); State.set('isPlaying', !State.get('isPlaying')); break; case 'delete': case 'backspace': if (sel) sm?.removeMesh(sel); break; case 'escape': if (!State.get('inspectorLocked')) State.set('selectedMeshName', null); UIContextMenu.hide(); break; case 'h': if (sel) { const m=sm?.getMesh(sel); if(m) m.isVisible=!m.isVisible; } break; case 'g': // Toggle grid document.getElementById('grid-btn').click(); break; case '1': sm?.camera && (sm.camera.alpha=-Math.PI/4, sm.camera.beta=Math.PI/3.5, sm.camera.radius=12); break; case '2': sm?.camera && (sm.camera.alpha=0, sm.camera.beta=0.01, sm.camera.radius=12); break; case '3': sm?.camera && (sm.camera.alpha=-Math.PI/2, sm.camera.beta=Math.PI/3.5, sm.camera.radius=12); break; } }); } /* ──────────────────────────────────────────────────────────── §21 UTILITIES ──────────────────────────────────────────────────────────── */ function rgbToHex(r, g, b) { const h = v => Math.round(Math.max(0,Math.min(1,v))*255).toString(16).padStart(2,'0'); return `#${h(r)}${h(g)}${h(b)}`; } function hexToRgb(hex) { const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!m) return [0.7,0.7,0.75]; return [parseInt(m[1],16)/255, parseInt(m[2],16)/255, parseInt(m[3],16)/255]; } function esc(s) { return String(s) .replace(/&/g,'&').replace(//g,'>') .replace(/"/g,'"').replace(/'/g,'''); } /* ──────────────────────────────────────────────────────────── §22 APP INITIALIZATION ──────────────────────────────────────────────────────────── */ async function initApp() { setLoadProgress(8, 'Setting up UI…'); // Wire all UI events first (console needs to be ready) wireEvents(); setupKeyboard(); setLoadProgress(20, 'Loading Monaco editor…'); setupMonaco(); // async — Monaco loads in background setLoadProgress(35, 'Initializing BabylonJS…'); await new Promise(r => setTimeout(r, 80)); const canvas = document.getElementById('renderCanvas'); let engineOk = false; try { await Engine.init(canvas); engineOk = true; document.getElementById('vp-fallback').classList.add('hidden'); setLoadProgress(65, 'Building scene…'); } catch(e) { logToConsole('warn', `WebGL/WebGPU unavailable: ${e.message}`, 'Engine'); setLoadProgress(65, 'UI-only mode…'); } await new Promise(r => setTimeout(r, 60)); setLoadProgress(80, 'Populating panels…'); UIHierarchy.refresh(); UIScriptEditor.refreshFileTree(); // Subscribe hierarchy refresh to mesh state changes State.sub('sceneId', () => UIHierarchy.refresh()); setLoadProgress(92, 'Finalizing…'); await new Promise(r => setTimeout(r, 80)); setLoadProgress(100, 'Ready.'); await new Promise(r => setTimeout(r, 280)); // Dismiss loading screen document.getElementById('loading').classList.add('hidden'); document.getElementById('app').classList.add('visible'); // Periodic hierarchy sync (in case meshes are added via scripts) setInterval(() => { if (State.get('activeTab') === 'scene') UIHierarchy.refresh(); }, 2000); // Log welcome message setTimeout(() => { logToConsole('info', '══════════════════════════════════════════', 'Engine'); logToConsole('info', ' Visigence Studio v1.0 — Ready', 'Engine'); logToConsole('info', ' © 2026 Omry Damari. All Rights Reserved.', 'Engine'); logToConsole('info', '══════════════════════════════════════════', 'Engine'); logToConsole('info', ' Press Space to Play/Stop scripts.', 'Engine'); logToConsole('info', ' W/E/R — Translate/Rotate/Scale gizmo.', 'Engine'); logToConsole('info', ' F — Focus on selected object.', 'Engine'); logToConsole('info', ' Del — Delete selected object.', 'Engine'); logToConsole('info', ' Ctrl+D — Duplicate selected.', 'Engine'); logToConsole('info', ' Ctrl+Z / Ctrl+Y — Undo / Redo.', 'Engine'); logToConsole('info', '══════════════════════════════════════════', 'Engine'); }, 600); } // Boot window.addEventListener('load', () => { initApp().catch(err => { console.error('Visigence Studio init error:', err); setLoadProgress(100, 'Init failed — check DevTools console'); document.getElementById('loading').classList.add('hidden'); document.getElementById('app').classList.add('visible'); }); });