forge3d / static /index.html
VISHAL18for4's picture
Upload index.html
8aa1dcc verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>FORGE3D v4.0</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{width:100%;height:100%;overflow:hidden;background:#1a1a1a;font-family:'Segoe UI',system-ui,sans-serif;font-size:12px;color:#ccc;user-select:none;}
#app{display:flex;flex-direction:column;width:100%;height:100%;}
#topbar{height:30px;background:#111;border-bottom:1px solid #000;display:flex;align-items:stretch;flex-shrink:0;z-index:300;}
.logo{display:flex;align-items:center;gap:5px;padding:0 10px;border-right:1px solid #222;flex-shrink:0;}
.logo-sq{width:18px;height:18px;background:#e87d39;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:900;color:#fff;}
.logo-nm{font-weight:700;font-size:12px;color:#ddd;letter-spacing:1px;}
.logo-home{padding:0 8px;display:flex;align-items:center;color:#555;text-decoration:none;font-size:11px;border-right:1px solid #222;}
.logo-home:hover{color:#e87d39;}
.mi{position:relative;display:flex;align-items:center;}
.mb{height:100%;padding:0 10px;background:none;border:none;color:#bbb;cursor:pointer;font-size:12px;font-family:inherit;}
.mb:hover,.mb.open{background:#2a2a2a;color:#fff;}
.md{display:none;position:fixed;top:30px;background:#252525;border:1px solid #3a3a3a;border-radius:5px;min-width:190px;z-index:9999;box-shadow:0 8px 32px rgba(0,0,0,.9);padding:3px 0;}
.md.show{display:block;}
.mdi{display:flex;justify-content:space-between;align-items:center;padding:6px 14px;cursor:pointer;font-size:12px;color:#bbb;}
.mdi:hover{background:#3a6ea8;color:#fff;}
.mdi .ks{color:#555;font-size:10px;}.mdi:hover .ks{color:#aaa;}
.msp{height:1px;background:#333;margin:3px 0;}
.msb{padding:2px 14px;color:#555;font-size:10px;}
#modebar{height:32px;background:#1e1e1e;border-bottom:1px solid #111;display:flex;align-items:center;padding:0 6px;gap:2px;flex-shrink:0;}
.mbtn{padding:3px 9px;background:#2a2a2a;border:1px solid #383838;border-radius:4px;color:#aaa;cursor:pointer;font-size:12px;font-family:inherit;}
.mbtn:hover{background:#333;color:#ddd;}.mbtn.on{background:#e87d39;border-color:#e87d39;color:#fff;}
.msep2{width:1px;height:18px;background:#333;margin:0 4px;flex-shrink:0;}
.shdr{display:flex;gap:2px;margin-left:auto;}
.sb{width:26px;height:24px;background:#2a2a2a;border:1px solid #383838;border-radius:4px;cursor:pointer;color:#aaa;font-size:13px;display:flex;align-items:center;justify-content:center;}
.sb:hover{background:#333;color:#ddd;}.sb.on{background:#3a6ea8;border-color:#4a80c0;color:#fff;}
#tabbar{height:26px;background:#161616;border-bottom:1px solid #111;display:flex;align-items:flex-end;padding:0 8px;gap:1px;flex-shrink:0;}
.tbt{padding:3px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:11px;color:#666;border:1px solid transparent;border-bottom:none;position:relative;top:1px;}
.tbt:hover{color:#bbb;background:#1e1e1e;}.tbt.on{color:#ddd;background:#1e1e1e;border-color:#333;border-bottom:1px solid #1e1e1e;}
#main{flex:1;display:flex;min-height:0;overflow:hidden;}
#ltbar{width:38px;background:#1a1a1a;border-right:1px solid #111;display:flex;flex-direction:column;align-items:center;padding:3px 0;gap:1px;flex-shrink:0;overflow:hidden;}
.tb{width:34px;height:32px;background:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer;color:#888;font-size:15px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.tb:hover{background:#2a2a2a;color:#ccc;}.tb.on{background:#e87d39;color:#fff;border-color:#e87d39;}
.tsep{width:30px;height:1px;background:#2a2a2a;margin:1px 0;flex-shrink:0;}
#vpc{flex:1;display:flex;flex-direction:column;min-width:0;min-height:0;}
.tc{flex:1;display:none;min-height:0;overflow:hidden;}
.tc.on{display:flex;flex-direction:column;}
#vpw{flex:1;position:relative;overflow:hidden;min-height:0;background:#3a3a3a;}
#c{display:block;width:100%;height:100%;touch-action:none;}
#vbadge{position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.7);padding:3px 14px;border-radius:20px;font-size:11px;color:#ddd;pointer-events:none;z-index:10;white-space:nowrap;}
#vcorner{position:absolute;top:8px;right:8px;display:flex;gap:3px;z-index:10;}
.vcb{width:26px;height:26px;background:rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.12);border-radius:4px;cursor:pointer;color:#bbb;font-size:13px;display:flex;align-items:center;justify-content:center;}
.vcb:hover{background:rgba(255,255,255,.15);color:#fff;}
#giz{position:absolute;bottom:12px;right:12px;width:68px;height:68px;z-index:10;pointer-events:none;}
#rp{width:240px;background:#1e1e1e;border-left:1px solid #111;display:flex;flex-direction:column;flex-shrink:0;transition:width .15s;overflow:hidden;}
#rp.hid{width:0;}
.rpt{display:flex;background:#111;border-bottom:1px solid #111;flex-shrink:0;overflow-x:auto;}
.rptb{padding:5px 7px;cursor:pointer;font-size:11px;color:#666;border-bottom:2px solid transparent;white-space:nowrap;flex-shrink:0;}
.rptb.on{color:#ddd;border-bottom-color:#e87d39;}.rptb:hover{color:#bbb;}
.rpb{flex:1;overflow-y:auto;display:none;}.rpb.on{display:block;}
.oli{display:flex;align-items:center;padding:4px 8px;gap:5px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.03);}
.oli:hover{background:#2a2a2a;}.oli.sel{background:#1f3d6e;}
.eye{cursor:pointer;opacity:.5;font-size:11px;margin-left:auto;}.eye:hover{opacity:1;}
.ob{display:flex;flex-wrap:wrap;gap:4px;padding:6px;border-top:1px solid #222;}
.btn{padding:3px 7px;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;color:#bbb;cursor:pointer;font-size:11px;font-family:inherit;}
.btn:hover{background:#333;}.btn.r:hover{background:#7a1a1a;color:#f55;}
.ps{border-bottom:1px solid #111;}
.ph{padding:5px 8px;background:#161616;cursor:pointer;font-size:11px;font-weight:600;color:#bbb;}
.pb{padding:7px;display:grid;grid-template-columns:auto 1fr;gap:4px 8px;align-items:center;}
.pl{color:#666;font-size:11px;white-space:nowrap;}
.pi{background:#111;border:1px solid #2a2a2a;border-radius:3px;color:#ddd;padding:3px 5px;font-size:11px;width:100%;font-family:inherit;}
.pi:focus{outline:none;border-color:#e87d39;}.pi[readonly]{opacity:.5;}
.xyz{display:grid;grid-template-columns:1fr 1fr 1fr;gap:2px;}
.xyz .pi{font-size:10px;padding:2px 3px;}
.xyzl{display:grid;grid-template-columns:1fr 1fr 1fr;gap:2px;margin-bottom:1px;}
.cx{color:#e44;font-size:9px;text-align:center;}.cy{color:#4e4;font-size:9px;text-align:center;}.cz{color:#55f;font-size:9px;text-align:center;}
.rs{padding:8px;border-bottom:1px solid #111;}
.rs h3{font-size:11px;color:#888;margin-bottom:6px;font-weight:600;}
.rr{display:flex;align-items:center;gap:6px;margin-bottom:5px;}
.rr .pl{width:80px;flex-shrink:0;}
.rbtn{width:100%;padding:7px;background:#e87d39;border:none;border-radius:5px;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;margin-top:4px;}
.rbtn:hover{background:#ff9955;}
.st{display:flex;flex-wrap:wrap;gap:4px;padding:8px;}
.stb{padding:5px 8px;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;color:#bbb;cursor:pointer;font-size:11px;font-family:inherit;flex:1;text-align:center;min-width:70px;}
.stb:hover{background:#333;}.stb.on{background:#3a6ea8;color:#fff;}
.sct{height:28px;background:#1a1a1a;border-bottom:1px solid #222;display:flex;align-items:center;padding:0 8px;gap:5px;flex-shrink:0;}
.scb{padding:2px 8px;background:#252525;border:1px solid #333;border-radius:3px;color:#bbb;font-size:11px;cursor:pointer;font-family:inherit;}
.scb:hover{background:#333;color:#fff;}
#sa{flex:1;background:#0d0d0d;color:#abb2bf;font-size:12px;font-family:'Courier New',monospace;padding:10px;border:none;outline:none;resize:none;line-height:1.6;}
#so{height:70px;background:#080808;color:#888;font-size:11px;font-family:monospace;padding:6px;border-top:1px solid #222;overflow-y:auto;}
#tlw{position:relative;background:#1a1a1a;border-top:1px solid #111;flex-shrink:0;display:flex;flex-direction:column;height:85px;}
#tlr{height:5px;cursor:ns-resize;background:transparent;position:absolute;top:0;left:0;right:0;z-index:20;}
#tlr:hover{background:rgba(232,125,57,.3);}
#tlc{height:28px;display:flex;align-items:center;padding:0 6px;gap:2px;background:#161616;border-bottom:1px solid #111;flex-shrink:0;overflow-x:auto;}
.tlb{width:24px;height:22px;background:#252525;border:1px solid #333;border-radius:3px;cursor:pointer;color:#888;font-size:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.tlb:hover{background:#333;color:#ddd;}.tlb.on{background:#e87d39;border-color:#e87d39;color:#fff;}
.tli{width:44px;padding:2px 4px;background:#111;border:1px solid #333;border-radius:3px;color:#ddd;font-size:11px;text-align:center;font-family:inherit;flex-shrink:0;}
.tls{width:1px;height:16px;background:#333;margin:0 3px;flex-shrink:0;}
.tlk{padding:2px 6px;background:#252525;border:1px solid #333;border-radius:3px;color:#888;font-size:11px;cursor:pointer;font-family:inherit;flex-shrink:0;}
.tlk:hover{background:#e87d39;color:#fff;border-color:#e87d39;}
#tlt{flex:1;display:flex;min-height:0;overflow:hidden;}
#tll{width:60px;background:#111;border-right:1px solid #222;display:flex;flex-direction:column;justify-content:space-around;padding:1px 4px;flex-shrink:0;}
.tll{font-size:9px;}
#tlcv{flex:1;display:block;cursor:crosshair;}
#sb{height:18px;background:#0d0d0d;border-top:1px solid #000;display:flex;align-items:center;padding:0 8px;gap:10px;font-size:10px;flex-shrink:0;color:#555;}
.sv{color:#e87d39;font-weight:700;}.sv2{color:#777;}
#ctx{position:fixed;background:#252525;border:1px solid #3a3a3a;border-radius:5px;z-index:9999;display:none;box-shadow:0 8px 28px rgba(0,0,0,.9);padding:3px 0;min-width:160px;}
#ctx.show{display:block;}
.cxi{padding:6px 14px;cursor:pointer;font-size:12px;color:#bbb;display:flex;justify-content:space-between;}
.cxi:hover{background:#3a6ea8;color:#fff;}
#toast{position:fixed;bottom:90px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.9);color:#ddd;padding:5px 16px;border-radius:20px;font-size:12px;z-index:9999;display:none;pointer-events:none;border:1px solid rgba(255,255,255,.1);white-space:nowrap;max-width:90%;}
/* AI */
#aifab{position:fixed;right:16px;bottom:46px;width:48px;height:48px;background:linear-gradient(135deg,#e87d39,#a855f7);border:none;border-radius:50%;color:#fff;font-size:20px;cursor:pointer;z-index:9998;box-shadow:0 4px 20px rgba(168,85,247,.5);}
#aifab:hover{transform:scale(1.08);}
#aip{position:fixed;right:12px;bottom:104px;width:340px;background:#141414;border:1px solid #3a3a3a;border-radius:12px;z-index:9998;display:none;flex-direction:column;box-shadow:0 12px 48px rgba(0,0,0,.95);max-height:78vh;}
#aip.open{display:flex;}
#aih{padding:9px 14px;background:linear-gradient(135deg,#e87d39,#a855f7);border-radius:11px 11px 0 0;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;}
#aist{font-size:10px;background:rgba(0,0,0,.3);padding:2px 8px;border-radius:10px;color:#ddd;}
.amts{display:flex;background:#0d0d0d;border-bottom:1px solid #222;flex-shrink:0;}
.amt{flex:1;padding:6px 3px;text-align:center;cursor:pointer;font-size:10px;color:#555;border-bottom:2px solid transparent;line-height:1.4;}
.amt:hover{color:#bbb;}.amt.on{color:#fff;border-bottom-color:#e87d39;}
.aqr{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #1a1a1a;flex-shrink:0;}
.aqb{flex:1;padding:5px 3px;background:#1a1a1a;border:1px solid #252525;border-radius:6px;color:#777;font-size:10px;cursor:pointer;text-align:center;}
.aqb:hover{background:#252525;color:#ddd;border-color:#e87d39;}
#ams{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px;min-height:80px;max-height:220px;}
.amu{background:#e87d39;color:#fff;align-self:flex-end;padding:6px 10px;border-radius:12px 12px 3px 12px;max-width:85%;word-break:break-word;font-size:11px;}
.ama{background:#1e1e1e;color:#ccc;align-self:flex-start;padding:6px 10px;border-radius:12px 12px 12px 3px;max-width:90%;word-break:break-word;font-size:11px;border:1px solid #2a2a2a;white-space:pre-wrap;}
.ama.g{border-left:3px solid #ff6b35;}.ama.n{border-left:3px solid #76b900;}.ama.o{border-left:3px solid #a855f7;}.ama.e{border-left:3px solid #e44;color:#f88;}
.ath{display:flex;gap:4px;align-items:center;padding:5px 8px;}
.ad{width:6px;height:6px;border-radius:50%;background:#e87d39;animation:bop .8s infinite;}
.ad:nth-child(2){animation-delay:.15s;}.ad:nth-child(3){animation-delay:.3s;}
@keyframes bop{0%,80%,100%{transform:scale(.8);opacity:.5;}40%{transform:scale(1.2);opacity:1;}}
#air{padding:7px;border-top:1px solid #1a1a1a;display:flex;gap:5px;flex-shrink:0;}
#aii{flex:1;padding:6px 9px;background:#0d0d0d;border:1px solid #2a2a2a;border-radius:8px;color:#ddd;font-size:12px;font-family:inherit;outline:none;}
#aii:focus{border-color:#e87d39;}
#ais{padding:6px 13px;background:#e87d39;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:12px;font-weight:700;font-family:inherit;}
#ais:hover{background:#ff9955;}#ais:disabled{opacity:.5;cursor:default;}
#apre{background:#080808;border-top:1px solid #111;padding:5px 8px;font-size:10px;font-family:monospace;color:#4e4;max-height:44px;overflow:hidden;display:none;flex-shrink:0;}
#asugs{padding:0 8px 7px;display:flex;flex-wrap:wrap;gap:4px;flex-shrink:0;}
.asg{padding:3px 8px;background:#1a1a1a;border:1px solid #252525;border-radius:12px;color:#666;font-size:10px;cursor:pointer;}
.asg:hover{background:#252525;color:#ddd;border-color:#e87d39;}
/* Texture dialog */
#tdlg{background:#0f0f0f;border-top:1px solid #222;padding:9px;display:none;flex-shrink:0;}
#tdlg.show{display:block;}
.tr{display:flex;gap:5px;margin-bottom:6px;}
#ti{flex:1;padding:5px 8px;background:#1a1a1a;border:1px solid #252525;border-radius:6px;color:#ddd;font-size:11px;font-family:inherit;outline:none;}
#ti:focus{border-color:#e87d39;}
#tgo{padding:5px 11px;background:#3a6ea8;border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:11px;font-family:inherit;}
/* Anim form */
#aaf{padding:9px;display:none;flex-direction:column;gap:6px;border-top:1px solid #1a1a1a;background:#0a0a0a;flex-shrink:0;overflow-y:auto;max-height:280px;}
#aaf.show{display:flex;}
.afr{display:flex;align-items:flex-start;gap:8px;}
.afl{color:#555;font-size:10px;width:85px;flex-shrink:0;padding-top:4px;}
.afi{flex:1;padding:4px 7px;background:#111;border:1px solid #252525;border-radius:4px;color:#ddd;font-size:11px;font-family:inherit;outline:none;}
.afi:focus{border-color:#e87d39;}
.afh{color:#aaa;font-size:11px;font-weight:600;border-bottom:1px solid #1a1a1a;padding-bottom:5px;display:flex;justify-content:space-between;align-items:center;}
.afg{width:100%;padding:8px;background:linear-gradient(135deg,#e87d39,#a855f7);border:none;border-radius:7px;color:#fff;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;}
.afg:hover{opacity:.9;}
::-webkit-scrollbar{width:4px;height:4px;}::-webkit-scrollbar-track{background:#111;}::-webkit-scrollbar-thumb{background:#333;border-radius:2px;}
@media(max-width:500px){.logo-nm{display:none;}#rp{width:200px;}#aip{width:calc(100vw - 16px);right:8px;}}
</style>
</head>
<body>
<div id="app">
<div id="topbar">
<div class="logo"><div class="logo-sq">F</div><div class="logo-nm">FORGE3D</div></div>
<a class="logo-home" href="/">๐Ÿ </a>
<div class="mi"><button class="mb" onclick="openMenu(this,'md0')">File</button>
<div class="md" id="md0">
<div class="mdi" onclick="act('newScene')">New Scene<span class="ks">Ctrl+N</span></div>
<div class="msp"></div>
<div class="mdi" onclick="act('saveRender')">Render PNG<span class="ks">F12</span></div>
<div class="mdi" onclick="act('exportObj')">Export OBJ</div>
</div></div>
<div class="mi"><button class="mb" onclick="openMenu(this,'md1')">Edit</button>
<div class="md" id="md1">
<div class="mdi" onclick="act('undo')">Undo<span class="ks">Ctrl+Z</span></div>
<div class="msp"></div>
<div class="mdi" onclick="act('dup')">Duplicate<span class="ks">Ctrl+D</span></div>
<div class="mdi" onclick="act('del')">Delete<span class="ks">Del</span></div>
</div></div>
<div class="mi"><button class="mb" onclick="openMenu(this,'md2')">Add</button>
<div class="md" id="md2">
<div class="msb">โ”€โ”€ MESH โ”€โ”€</div>
<div class="mdi" onclick="addObj('cube');closeMenus()">โ–ฃ Cube</div>
<div class="mdi" onclick="addObj('sphere');closeMenus()">โฌค Sphere</div>
<div class="mdi" onclick="addObj('cylinder');closeMenus()">โ–ฌ Cylinder</div>
<div class="mdi" onclick="addObj('plane');closeMenus()">โ–ญ Plane</div>
<div class="mdi" onclick="addObj('cone');closeMenus()">โ–ฒ Cone</div>
<div class="mdi" onclick="addObj('torus');closeMenus()">โ—Ž Torus</div>
<div class="mdi" onclick="addObj('ico');closeMenus()">โ—‡ Icosphere</div>
<div class="mdi" onclick="addObj('torusknot');closeMenus()">โœฆ Torus Knot</div>
<div class="msp"></div>
<div class="mdi" onclick="addLight('point');closeMenus()">๐Ÿ’ก Point Light</div>
<div class="mdi" onclick="addLight('sun');closeMenus()">โ˜€ Sun Light</div>
<div class="mdi" onclick="addLight('spot');closeMenus()">๐Ÿ”ฆ Spot Light</div>
</div></div>
<div class="mi"><button class="mb" onclick="openMenu(this,'md3')">Object</button>
<div class="md" id="md3">
<div class="mdi" onclick="act('dup')">Duplicate</div>
<div class="mdi" onclick="act('del')">Delete</div>
<div class="msp"></div>
<div class="mdi" onclick="act('resetLoc')">Reset Location</div>
<div class="mdi" onclick="act('resetRot')">Reset Rotation</div>
<div class="mdi" onclick="act('resetScale')">Reset Scale</div>
<div class="mdi" onclick="act('mirror_x')">Mirror X</div>
<div class="mdi" onclick="act('snapGround')">Snap to Ground</div>
<div class="msp"></div>
<div class="mdi" onclick="act('focus')">Focus<span class="ks">.</span></div>
</div></div>
<div class="mi"><button class="mb" onclick="openMenu(this,'md4')">Render</button>
<div class="md" id="md4">
<div class="mdi" onclick="act('saveRender')">Render Image<span class="ks">F12</span></div>
<div class="msp"></div>
<div class="mdi" onclick="setShd('solid');closeMenus()">โ—ผ Solid</div>
<div class="mdi" onclick="setShd('wire');closeMenus()">โฌก Wireframe</div>
<div class="mdi" onclick="setShd('xray');closeMenus()">โ—ˆ X-Ray</div>
</div></div>
<div class="mi"><button class="mb" onclick="openMenu(this,'md5')">Window</button>
<div class="md" id="md5">
<div class="mdi" onclick="toggleRP();closeMenus()">Toggle Side Panel</div>
<div class="mdi" onclick="toggleTL();closeMenus()">Toggle Timeline</div>
<div class="mdi" onclick="toggleAI();closeMenus()">Toggle AI</div>
<div class="msp"></div>
<div class="mdi" onclick="switchTab('vp');closeMenus()">๐ŸŽฌ 3D Viewport</div>
<div class="mdi" onclick="switchTab('sc');closeMenus()">๐Ÿ“ Script</div>
</div></div>
<div class="mi"><button class="mb" onclick="openMenu(this,'md6')">Help</button>
<div class="md" id="md6">
<div class="mdi" onclick="showHelp();closeMenus()">Shortcuts</div>
<div class="mdi" onclick="chkStatus();closeMenus()">Check AI Status</div>
</div></div>
</div>
<div id="modebar">
<button class="mbtn on" id="mb-object" onclick="setMode('object')">Object</button>
<button class="mbtn" id="mb-edit" onclick="setMode('edit')">Edit</button>
<button class="mbtn" id="mb-sculpt" onclick="setMode('sculpt')">Sculpt</button>
<div class="msep2"></div>
<select style="padding:2px 5px;background:#2a2a2a;border:1px solid #383838;border-radius:4px;color:#aaa;font-size:11px;font-family:inherit;">
<option>Median Point</option><option>Individual</option><option>Active</option>
</select>
<div class="shdr">
<button class="sb on" id="shd-solid" onclick="setShd('solid')" title="Solid">โ—ผ</button>
<button class="sb" id="shd-wire" onclick="setShd('wire')" title="Wireframe">โฌก</button>
<button class="sb" id="shd-xray" onclick="setShd('xray')" title="X-Ray">โ—ˆ</button>
</div>
</div>
<div id="tabbar">
<div class="tbt on" id="tabt-vp" onclick="switchTab('vp')">๐ŸŽฌ 3D Viewport</div>
<div class="tbt" id="tabt-sc" onclick="switchTab('sc')">๐Ÿ“ Script</div>
<div class="tbt" id="tabt-sh" onclick="switchTab('sh')">โฌก Shader</div>
</div>
<div id="main">
<div id="ltbar">
<button class="tb on" id="t-select" onclick="setTool('select')" title="Select W">โ†–</button>
<button class="tb" id="t-move" onclick="setTool('move')" title="Move G">โœฅ</button>
<button class="tb" id="t-rotate" onclick="setTool('rotate')" title="Rotate R">โ†ป</button>
<button class="tb" id="t-scale" onclick="setTool('scale')" title="Scale S">โคก</button>
<div class="tsep"></div>
<button class="tb" id="t-grab_x" onclick="setTool('grab_x')" title="Move X">โ†”X</button>
<button class="tb" id="t-grab_y" onclick="setTool('grab_y')" title="Move Y">โ†•Y</button>
<button class="tb" id="t-grab_z" onclick="setTool('grab_z')" title="Move Z">โ†•Z</button>
<div class="tsep"></div>
<button class="tb" onclick="act('focus')" title="Focus .">โ—Ž</button>
<button class="tb" onclick="act('viewAll')" title="View All">โŠŸ</button>
<button class="tb" onclick="act('toggleOrtho')" title="Ortho 5">๐Ÿ“</button>
<div class="tsep"></div>
<button class="tb" onclick="setView('front')" title="Front 1">โ‘ </button>
<button class="tb" onclick="setView('side')" title="Side 3">โ‘ข</button>
<button class="tb" onclick="setView('top')" title="Top 7">โ‘ฆ</button>
<div class="tsep"></div>
<button class="tb" onclick="addObj('cube')" title="Add Cube">โ–ฃ</button>
<button class="tb" onclick="act('del')" title="Delete" style="color:#c55;">โœ•</button>
<div class="tsep"></div>
<button class="tb" onclick="toggleAI()" title="AI" style="color:#a855f7;font-size:17px;">๐Ÿค–</button>
</div>
<div id="vpc">
<div class="tc on" id="tc-vp"><div id="vpw"><canvas id="c"></canvas>
<div id="vbadge">Object ยท Select ยท RMB=Orbit ยท Scroll=Zoom ยท Shift+RMB=Pan</div>
<div id="vcorner">
<button class="vcb" onclick="act('focus')">โ—Ž</button>
<button class="vcb" onclick="act('toggleOrtho')">๐Ÿ“</button>
<button class="vcb" onclick="toggleRP()">โ–ถ</button>
<button class="vcb" onclick="toggleAI()" style="color:#a855f7;">๐Ÿค–</button>
</div>
<canvas id="giz" width="68" height="68"></canvas>
</div></div>
<div class="tc" id="tc-sc"><div style="flex:1;display:flex;flex-direction:column;">
<div class="sct">
<button class="scb" onclick="runSc()">โ–ถ Run</button>
<button class="scb" onclick="clrSc()">Clear</button>
<button class="scb" onclick="insTpl('solar')">๐ŸŒ Solar</button>
<button class="scb" onclick="insTpl('blackhole')">๐ŸŒ‘ Black Hole</button>
<button class="scb" onclick="insTpl('missile')">๐Ÿš€ Missile</button>
<button class="scb" onclick="insTpl('cubes')">๐ŸŽฒ Cubes</button>
<span style="margin-left:auto;color:#333;font-size:10px;padding-right:4px;">addObjยทaddLightยทactยทtoastยทSCENEยทTHREEยทOBJS</span>
</div>
<textarea id="sa" spellcheck="false">// FORGE3D Script โ€” AI code saves here automatically
// addObj(type), addLight(type), act(cmd), toast(msg), SCENE, THREE, OBJS
for(let i=0;i<5;i++){
const o=addObj('cube');
o.mesh.position.set((Math.random()-.5)*8,Math.random()*3+.5,(Math.random()-.5)*8);
o.mesh.material.color.setHSL(i/5,.7,.5);
o.mesh.material.metalness=0.6; o.mesh.material.roughness=0.3;
}
toast('5 cubes!');</textarea>
<div id="so">// Console output</div>
</div></div>
<div class="tc" id="tc-sh"><div style="flex:1;display:flex;align-items:center;justify-content:center;background:#0d0d0d;color:#444;flex-direction:column;gap:10px;"><span style="font-size:28px;">โฌก</span><span>Use Material tab for PBR presets</span></div></div>
</div>
<div id="rp">
<div class="rpt">
<div class="rptb on" id="rpt-ol" onclick="setRP('ol')">Outliner</div>
<div class="rptb" id="rpt-pr" onclick="setRP('pr')">Props</div>
<div class="rptb" id="rpt-mat" onclick="setRP('mat')">Material</div>
<div class="rptb" id="rpt-ren" onclick="setRP('ren')">Render</div>
<div class="rptb" id="rpt-sc" onclick="setRP('sc')">Sculpt</div>
</div>
<div class="rpb on" id="rpb-ol">
<div style="padding:5px;border-bottom:1px solid #111;">
<input type="text" placeholder="๐Ÿ” Filter..." oninput="filterOL(this.value)"
style="width:100%;padding:3px 7px;background:#111;border:1px solid #333;border-radius:4px;color:#ddd;font-size:11px;font-family:inherit;outline:none;">
</div>
<div id="ollist"></div>
<div class="ob">
<button class="btn" onclick="addObj('cube')">+Cube</button>
<button class="btn" onclick="addObj('sphere')">+Sphere</button>
<button class="btn" onclick="addObj('plane')">+Plane</button>
<button class="btn" onclick="addLight('point')">+Light</button>
<button class="btn" onclick="act('dup')">Dup</button>
<button class="btn r" onclick="act('del')">๐Ÿ—‘</button>
</div>
</div>
<div class="rpb" id="rpb-pr">
<div id="pno" style="padding:20px;color:#555;text-align:center;font-size:11px;">No object selected</div>
<div id="pbd" style="display:none;">
<div class="ps"><div class="ph" onclick="toggleSec('s-obj')">โ–พ Object</div>
<div class="pb" id="s-obj">
<div class="pl">Name</div><input class="pi" id="pNm" oninput="setProp('name',this.value)">
<div class="pl">Type</div><input class="pi" id="pTy" readonly>
</div></div>
<div class="ps"><div class="ph" onclick="toggleSec('s-tr')">โ–พ Transform</div>
<div class="pb" id="s-tr">
<div class="pl">Location</div>
<div><div class="xyzl"><span class="cx">X</span><span class="cy">Y</span><span class="cz">Z</span></div>
<div class="xyz">
<input class="pi" id="pLX" type="number" step="0.1" oninput="setProp('lx',+this.value)">
<input class="pi" id="pLY" type="number" step="0.1" oninput="setProp('ly',+this.value)">
<input class="pi" id="pLZ" type="number" step="0.1" oninput="setProp('lz',+this.value)">
</div></div>
<div class="pl">Rotationยฐ</div>
<div><div class="xyzl"><span class="cx">X</span><span class="cy">Y</span><span class="cz">Z</span></div>
<div class="xyz">
<input class="pi" id="pRX" type="number" step="1" oninput="setProp('rx',+this.value)">
<input class="pi" id="pRY" type="number" step="1" oninput="setProp('ry',+this.value)">
<input class="pi" id="pRZ" type="number" step="1" oninput="setProp('rz',+this.value)">
</div></div>
<div class="pl">Scale</div>
<div><div class="xyzl"><span class="cx">X</span><span class="cy">Y</span><span class="cz">Z</span></div>
<div class="xyz">
<input class="pi" id="pSX" type="number" step="0.1" oninput="setProp('sx',+this.value)">
<input class="pi" id="pSY" type="number" step="0.1" oninput="setProp('sy',+this.value)">
<input class="pi" id="pSZ" type="number" step="0.1" oninput="setProp('sz',+this.value)">
</div></div>
</div></div>
</div>
</div>
<div class="rpb" id="rpb-mat">
<div id="mno" style="padding:20px;color:#555;text-align:center;font-size:11px;">Select a mesh</div>
<div id="mbd" style="display:none;">
<div class="pb">
<div class="pl">Color</div><input class="pi" id="pCol" type="color" style="padding:1px 2px;height:24px;" oninput="setProp('color',this.value)">
<div class="pl">Metallic</div><input class="pi" id="pMet" type="range" min="0" max="1" step="0.01" style="padding:4px 0;" oninput="setProp('metal',+this.value)">
<div class="pl">Roughness</div><input class="pi" id="pRgh" type="range" min="0" max="1" step="0.01" style="padding:4px 0;" oninput="setProp('rough',+this.value)">
<div class="pl">Emission</div><input class="pi" id="pEmt" type="range" min="0" max="5" step="0.01" style="padding:4px 0;" oninput="setProp('emit',+this.value)">
<div class="pl">Alpha</div><input class="pi" id="pAlp" type="range" min="0" max="1" step="0.01" style="padding:4px 0;" oninput="setProp('alpha',+this.value)">
<div class="pl">Texture</div>
<button class="btn" onclick="document.getElementById('texi').click()" style="width:100%;">๐Ÿ“‚ Load Image</button>
</div>
<div style="padding:8px;border-top:1px solid #111;">
<div style="color:#555;font-size:10px;margin-bottom:5px;text-transform:uppercase;letter-spacing:1px;">Presets</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;">
<button class="btn" onclick="applyPre('chrome')" style="font-size:10px;">Chrome</button>
<button class="btn" onclick="applyPre('gold')" style="font-size:10px;">Gold</button>
<button class="btn" onclick="applyPre('rubber')" style="font-size:10px;">Rubber</button>
<button class="btn" onclick="applyPre('glass')" style="font-size:10px;">Glass</button>
<button class="btn" onclick="applyPre('neon')" style="font-size:10px;">Neon</button>
<button class="btn" onclick="applyPre('emit')" style="font-size:10px;">Glow</button>
<button class="btn" onclick="applyPre('lava')" style="font-size:10px;">Lava</button>
<button class="btn" onclick="applyPre('ice')" style="font-size:10px;">Ice</button>
<button class="btn" onclick="applyPre('steel')" style="font-size:10px;">Steel</button>
<button class="btn" onclick="applyPre('plasma')" style="font-size:10px;">Plasma</button>
</div>
</div>
</div>
</div>
<div class="rpb" id="rpb-ren">
<div class="rs"><h3>Output</h3>
<div class="rr"><div class="pl">Preset</div>
<select class="pi" onchange="setRP2(this.value)">
<option value="1920,1080">Full HD</option><option value="1280,720">HD</option>
<option value="3840,2160">4K</option><option value="1080,1080">Square</option>
</select></div>
<div class="rr"><div class="pl">Width</div><input class="pi" id="rW" type="number" value="1920"></div>
<div class="rr"><div class="pl">Height</div><input class="pi" id="rH" type="number" value="1080"></div>
<div class="rr"><div class="pl">Format</div><select class="pi" id="rFmt"><option>PNG</option><option>JPEG</option></select></div>
</div>
<div class="rs"><h3>Scene</h3>
<div class="rr"><div class="pl">BG Color</div><input type="color" class="pi" value="#3c3c3c" style="padding:1px 2px;height:24px;" oninput="SCENE.background.set(this.value)"></div>
<div class="rr"><div class="pl">Ambient</div><input class="pi" type="range" min="0" max="2" step="0.05" value="0.4" oninput="AMBLIGHT.intensity=+this.value"></div>
<div class="rr"><div class="pl">Sun</div><input class="pi" type="range" min="0" max="3" step="0.05" value="1.1" oninput="SUNLIGHT.intensity=+this.value"></div>
<div class="rr"><div class="pl">Exposure</div><input class="pi" type="range" min="0" max="3" step="0.05" value="1" oninput="RENDERER.toneMappingExposure=+this.value"></div>
</div>
<div style="padding:8px;"><button class="rbtn" onclick="act('saveRender')">๐ŸŽจ Render &amp; Save (F12)</button></div>
</div>
<div class="rpb" id="rpb-sc">
<div style="padding:8px;border-bottom:1px solid #111;color:#555;font-size:11px;">Select mesh โ†’ Sculpt Mode โ†’ Subdivide โ†’ Paint</div>
<div class="st">
<button class="stb on" id="sc-draw" onclick="setST('draw')">โœ Draw</button>
<button class="stb" id="sc-smooth" onclick="setST('smooth')">โ‰ˆ Smooth</button>
<button class="stb" id="sc-grab" onclick="setST('grab')">โœฅ Grab</button>
<button class="stb" id="sc-inflate" onclick="setST('inflate')">โŠ• Inflate</button>
<button class="stb" id="sc-flatten" onclick="setST('flatten')">โ–ฌ Flatten</button>
<button class="stb" id="sc-crease" onclick="setST('crease')">โŒ’ Crease</button>
</div>
<div class="pb">
<div class="pl">Size</div><input class="pi" id="sc-sz" type="range" min="0.1" max="5" step="0.1" value="1.2" style="padding:4px 0;">
<div class="pl">Strength</div><input class="pi" id="sc-st" type="range" min="0.01" max="1" step="0.01" value="0.35" style="padding:4px 0;">
<div class="pl">Direction</div>
<select class="pi" id="sc-dr"><option value="1">Add</option><option value="-1">Subtract</option></select>
<div class="pl">Subdivide</div>
<button class="btn" onclick="subdiv()" style="width:100%;">Subdivide Mesh</button>
</div>
</div>
</div>
</div>
<div id="tlw">
<div id="tlr"></div>
<div id="tlc">
<button class="tlb" onclick="tla('first')">โฎ</button>
<button class="tlb" onclick="tla('back')">โช</button>
<button class="tlb" id="tlplay" onclick="tla('play')">โ–ถ</button>
<button class="tlb" onclick="tla('fwd')">โฉ</button>
<button class="tlb" onclick="tla('last')">โญ</button>
<div class="tls"></div>
<input class="tli" id="tlf" type="number" min="1" value="1" oninput="setFr(+this.value)">
<span style="color:#444;padding:0 2px;flex-shrink:0;">/</span>
<input class="tli" id="tlend" type="number" value="250" oninput="FR_END=+this.value">
<div class="tls"></div>
<select style="padding:2px 4px;background:#252525;border:1px solid #333;border-radius:3px;color:#888;font-size:11px;font-family:inherit;flex-shrink:0;" onchange="FR_FPS=+this.value">
<option>12</option><option>24</option><option selected>25</option><option>30</option><option>60</option>
</select>
<span style="color:#555;font-size:10px;margin-left:2px;flex-shrink:0;">FPS</span>
<div class="tls"></div>
<button class="tlk" onclick="insKF('loc')">Loc</button>
<button class="tlk" onclick="insKF('rot')">Rot</button>
<button class="tlk" onclick="insKF('scl')">Scl</button>
<button class="tlk" onclick="insKF('all')">All</button>
<button class="tlk" onclick="delKF()" style="color:#c55;">Del KF</button>
<div class="tls"></div>
<label style="color:#555;font-size:10px;display:flex;align-items:center;gap:3px;flex-shrink:0;cursor:pointer;"><input type="checkbox" id="tlAKF" style="accent-color:#e87d39;"> Auto KF</label>
<span style="margin-left:auto;color:#2a2a2a;font-size:10px;padding-right:4px;flex-shrink:0;">Space=Play ยท I=KF</span>
</div>
<div id="tlt">
<div id="tll">
<div class="tll" style="color:#e44;">Loc X</div><div class="tll" style="color:#4e4;">Loc Y</div>
<div class="tll" style="color:#66f;">Loc Z</div><div class="tll" style="color:#ea4;">Rot X</div>
<div class="tll" style="color:#4ea;">Rot Y</div><div class="tll" style="color:#a4e;">Rot Z</div>
</div>
<canvas id="tlcv"></canvas>
</div>
</div>
<div id="sb">
<span class="sv">FORGE3D v4.0</span>
<span class="sv2">Mode:<b id="stm" style="color:#aaa;"> Object</b></span>
<span class="sv2">Tool:<b id="stt" style="color:#aaa;"> Select</b></span>
<span class="sv2">Objects:<b id="sto" style="color:#aaa;"> 0</b></span>
<span class="sv2">Verts:<b id="stv" style="color:#aaa;"> 0</b></span>
<span id="stfps" style="margin-left:auto;color:#444;">FPS:โ€”</span>
<a href="/" style="color:#3a3a3a;text-decoration:none;font-size:10px;margin-left:10px;">๐Ÿ </a>
</div>
</div>
<input type="file" id="texi" accept="image/*" style="display:none" onchange="applyTexF(this)">
<button id="aifab" onclick="toggleAI()">๐Ÿค–</button>
<div id="aip">
<div id="aih">
<div style="display:flex;align-items:center;gap:8px;"><span style="font-weight:700;color:#fff;font-size:13px;">๐Ÿค– FORGE3D AI</span><span id="aist">Checkingโ€ฆ</span></div>
<button onclick="toggleAI()" style="background:rgba(0,0,0,.3);border:none;color:#fff;cursor:pointer;width:24px;height:24px;border-radius:50%;font-size:14px;">โœ•</button>
</div>
<div class="amts">
<div class="amt on" id="at-groq" onclick="setAM('groq')"><span style="font-size:11px;">โšก Groq</span><br><span style="font-size:9px;color:#ff6b35;">LlamaยทFast</span></div>
<div class="amt" id="at-nemotron" onclick="setAM('nemotron')"><span style="font-size:11px;">๐ŸŸข Nemotron</span><br><span style="font-size:9px;color:#76b900;">NVIDIAยทScenes</span></div>
<div class="amt" id="at-openrouter" onclick="setAM('openrouter')"><span style="font-size:11px;">๐Ÿ”ฎ Detail AI</span><br><span style="font-size:9px;color:#a855f7;">Complexยท50pts</span></div>
</div>
<div class="aqr">
<div class="aqb" onclick="togTex()">๐ŸŽจ Texture</div>
<div class="aqb" onclick="genAITexture()">๐Ÿค– AI Tex</div>
<div class="aqb" onclick="togAF()">๐ŸŽฌ Animate</div>
<div class="aqb" onclick="quickAsk('remove all mesh objects keep lights add fresh ground plane')">๐Ÿ—‘ Clear</div>
<div class="aqb" onclick="switchTab('sc')">๐Ÿ“ Script</div>
<div class="aqb" onclick="chkStatus()">๐Ÿ“ก Status</div>
</div>
<div id="tdlg">
<div style="color:#555;font-size:10px;margin-bottom:5px;text-transform:uppercase;letter-spacing:1px;">Apply texture to selected:</div>
<div class="tr">
<input id="ti" type="text" placeholder="rusty metal, wood, concrete, lavaโ€ฆ" onkeydown="if(event.key==='Enter')sendTex()">
<button id="tgo" onclick="sendTex()">Apply</button>
</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;">
<span class="asg" onclick="qTex('brushed metal plate')">๐Ÿ”ฉ Metal</span>
<span class="asg" onclick="qTex('oak wood planks')">๐Ÿชต Wood</span>
<span class="asg" onclick="qTex('cracked concrete')">๐Ÿงฑ Concrete</span>
<span class="asg" onclick="qTex('lava glowing magma')">๐ŸŒ‹ Lava</span>
<span class="asg" onclick="qTex('white marble polished')">๐Ÿชจ Marble</span>
</div>
</div>
<div id="ams">
<div class="ama g">๐Ÿ‘‹ FORGE3D AI v4.0 ready!
๐Ÿš€ Missiles (15+ parts with fins, tubes, glow)
๐ŸŒ‘ Black holes (disks at multiple angles + jets)
๐ŸŒ Solar systems (real RAF orbital animation)
๐Ÿ›ธ Spaceships, cities, caves...
๐ŸŽจ Free PBR textures from Poly Haven
Code always saves to ๐Ÿ“ Script tab!</div>
</div>
<div id="apre"></div>
<div id="air">
<input id="aii" type="text" placeholder="Describe what to buildโ€ฆ" onkeydown="if(event.key==='Enter')sendAI()">
<button id="ais" onclick="sendAI()">Send</button>
</div>
<div id="asugs">
<div class="asg" onclick="quickAsk('build a detailed missile: LatheGeometry dark steel body, chrome ConeGeometry nose, 4 ExtrudeGeometry delta fins, black nozzle cylinder, emissive orange exhaust cylinder + torus glow ring, 4 TubeGeometry fuel lines, 16 chrome rivet spheres, red warhead band. 15+ parts total. Add orange PointLight.')">๐Ÿš€ Missile</div>
<div class="asg" onclick="quickAsk('build a dramatic black hole: large black SphereGeometry core emissive purple. 5 RingGeometry accretion disks each rotated differently (x rotation 0 to 1.2) emissive orange yellow red transparent. White TorusGeometry photon ring emissive intensity 5. Two TubeGeometry polar jets going up and down emissive blue transparent. 200 Points particles orbiting. Orange PointLight center. Animate with requestAnimationFrame: rotate core, spin particles, pulse rings.')">๐ŸŒ‘ Black Hole</div>
<div class="asg" onclick="quickAsk('animated solar system: clear scene first. emissive SphereGeometry Sun. PointLight sun glow. 8 planets unique colors sizes. RingGeometry orbit paths. Saturn with ring system child mesh. Asteroid belt 80 tiny random spheres at radius 28. Use requestAnimationFrame loop cos/sin for all orbital motion at different speeds. Ambient blue-dark light.')">๐ŸŒ Solar System</div>
<div class="asg" onclick="quickAsk('sci-fi spaceship: LatheGeometry dark steel hull, glass SphereGeometry cockpit transparent, 4 ExtrudeGeometry swept wings, 2 engine pod cylinders with emissive glow torus rings, weapon mount boxes, thin antenna cylinder, red+green running light spheres emissive, TubeGeometry thruster trails emissive blue. 15+ parts. Blue ambient light.')">๐Ÿ›ธ Spaceship</div>
<div class="asg" onclick="quickAsk('neon cyberpunk city night: 15 buildings varying heights box geometry, glowing neon sign planes emissive pink cyan, dark street grid plane, 4 colored PointLights, flying car box shapes above city, dark purple ambient atmosphere')">๐ŸŒ† Neon City</div>
</div>
<div id="aaf">
<div class="afh"><span>๐ŸŽฌ AI Animation</span><button onclick="togAF()" style="background:none;border:none;color:#555;cursor:pointer;font-size:14px;">โœ•</button></div>
<div class="afr"><div class="afl">Describe</div><textarea id="af-d" class="afi" rows="2" placeholder="planets orbit sun, missile launches upwardโ€ฆ" style="resize:none;"></textarea></div>
<div class="afr"><div class="afl">Duration</div><select id="af-dur" class="afi"><option value="5">5s</option><option value="10">10s</option><option value="30" selected>30s</option><option value="60">60s</option></select></div>
<div class="afr"><div class="afl">FPS</div><select id="af-fps" class="afi"><option value="24">24</option><option value="25" selected>25</option><option value="30">30</option></select></div>
<div class="afr"><div class="afl">Type</div><select id="af-ty" class="afi"><option value="auto">Auto</option><option value="orbit">Orbit/Solar</option><option value="rotation">Rotation</option><option value="bounce">Bounce</option></select></div>
<div class="afr"><div class="afl">Loop</div><input type="checkbox" id="af-lp" checked style="accent-color:#e87d39;width:14px;height:14px;"><span style="color:#555;font-size:10px;margin-left:4px;">Seamless</span></div>
<button class="afg" onclick="genAnim()">๐ŸŽฌ Generate Animation</button>
</div>
</div>
<div id="ctx">
<div class="cxi" onclick="act('del')">Delete<span style="color:#555;font-size:10px">Del</span></div>
<div class="cxi" onclick="act('dup')">Duplicate<span style="color:#555;font-size:10px">Ctrl+D</span></div>
<div class="cxi" onclick="act('focus')">Focus<span style="color:#555;font-size:10px">.</span></div>
<div style="height:1px;background:#333;margin:3px 0;"></div>
<div class="cxi" onclick="setTool('move')">Move (G)</div>
<div class="cxi" onclick="setTool('rotate')">Rotate (R)</div>
<div class="cxi" onclick="setTool('scale')">Scale (S)</div>
<div style="height:1px;background:#333;margin:3px 0;"></div>
<div class="cxi" onclick="addObj('cube')">Add Cube</div>
<div class="cxi" onclick="insKF('all')">Insert KF (I)</div>
</div>
<div id="toast"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script><script>
'use strict';
const D2R=Math.PI/180,R2D=180/Math.PI;
let SCENE,CAM,OCAM,RENDERER,RAYCASTER,AMBLIGHT,SUNLIGHT;
let OBJS=[],SEL=null,UND=[];
let MODE='object',TOOL='select',SHADING='solid',ORTHO=false;
let rpOpen=true,tlVis=true;
const ORB={th:.6,ph:1.1,r:10,tx:0,ty:.5,tz:0};
const PT={dn:false,btn:-1,lx:0,ly:0,sx:0,sy:0,drag:false};
let FR=1,FR_END=250,FR_FPS=25,TL_PLAY=false;
let KF={};
let SCULPT='draw',AI_M='groq',AI_OPEN=false;
let AI_H={groq:[],nemotron:[],openrouter:[]};
let _fc=0,_fl=performance.now();
window.addEventListener('DOMContentLoaded',()=>{
init3();buildScene();setupPtr();setupKeys();setupTL();setupTLR();
refreshOL();refreshSt();loop();
setTimeout(chkStatus,1000);
toast('FORGE3D v4.0 ยท ๐Ÿค– AI ready ยท ๐Ÿ“ Script tab visible above',4000);
});
// โ”€โ”€ THREE INIT โ”€โ”€
function init3(){
const cv=document.getElementById('c');
try{RENDERER=new THREE.WebGLRenderer({canvas:cv,antialias:true,preserveDrawingBuffer:true,powerPreference:'high-performance'});}
catch(e){RENDERER=new THREE.WebGLRenderer({canvas:cv,antialias:false,preserveDrawingBuffer:true});}
RENDERER.setPixelRatio(Math.min(devicePixelRatio,1.5)); // cap at 1.5 for performance
RENDERER.shadowMap.enabled=true;RENDERER.shadowMap.type=THREE.PCFSoftShadowMap;
RENDERER.outputEncoding=THREE.sRGBEncoding;RENDERER.toneMapping=THREE.CineonToneMapping;RENDERER.toneMappingExposure=1;
SCENE=new THREE.Scene();SCENE.background=new THREE.Color(0x3a3a3a);
CAM=new THREE.PerspectiveCamera(50,1,.01,5000);
OCAM=new THREE.OrthographicCamera(-5,5,5,-5,.01,5000);
RAYCASTER=new THREE.Raycaster();
new ResizeObserver(onResize).observe(document.getElementById('vpw'));
onResize();
}
function onResize(){
const w=document.getElementById('vpw');if(!w||!RENDERER)return;
const wd=w.clientWidth,ht=w.clientHeight;if(!wd||!ht)return;
RENDERER.setSize(wd,ht,false);CAM.aspect=wd/ht;CAM.updateProjectionMatrix();
const h=ORB.r*.45;OCAM.left=-h*(wd/ht);OCAM.right=h*(wd/ht);OCAM.top=h;OCAM.bottom=-h;OCAM.updateProjectionMatrix();
}
function getC(){return ORTHO?OCAM:CAM;}
function updCam(){
const{th,ph,r,tx,ty,tz}=ORB;
CAM.position.set(tx+r*Math.sin(ph)*Math.sin(th),ty+r*Math.cos(ph),tz+r*Math.sin(ph)*Math.cos(th));
CAM.lookAt(tx,ty,tz);OCAM.position.copy(CAM.position).multiplyScalar(1.5);OCAM.lookAt(tx,ty,tz);drawGiz();
}
function buildScene(){
AMBLIGHT=new THREE.AmbientLight(0xffffff,.4);SCENE.add(AMBLIGHT);
SUNLIGHT=new THREE.DirectionalLight(0xffffff,1.1);SUNLIGHT.position.set(6,12,5);SUNLIGHT.castShadow=true;
SUNLIGHT.shadow.mapSize.set(1024,1024);SUNLIGHT.shadow.camera.left=SUNLIGHT.shadow.camera.bottom=-30;
SUNLIGHT.shadow.camera.right=SUNLIGHT.shadow.camera.top=30;SUNLIGHT.shadow.camera.far=200;SCENE.add(SUNLIGHT);
SCENE.add(new THREE.DirectionalLight(0x8899ff,.2)).position.set(-6,3,-6);
SCENE.add(new THREE.GridHelper(200,80,0x555555,0x3a3a3a));
SCENE.add(new THREE.AxesHelper(5));
mkMesh('cube','Cube');
const lo=mkLight('point',false);if(lo){lo.mesh.position.set(4,5,3);lo.light.position.set(4,5,3);}
updCam();
}
// โ”€โ”€ FACTORIES โ”€โ”€
function mkMat(h){return new THREE.MeshStandardMaterial({color:h||'#888',roughness:.75,metalness:.05});}
function mkMesh(type,name,pos){
const N=28;let geo;
switch(type){
case'cube':geo=new THREE.BoxGeometry(2,2,2,2,2,2);break;
case'sphere':geo=new THREE.SphereGeometry(1,N,N/2);break;
case'cylinder':geo=new THREE.CylinderGeometry(1,1,2,N);break;
case'plane':geo=new THREE.PlaneGeometry(4,4,6,6);geo.rotateX(-Math.PI/2);break;
case'cone':geo=new THREE.ConeGeometry(1,2,N);break;
case'torus':geo=new THREE.TorusGeometry(1,.35,14,N);break;
case'ico':geo=new THREE.IcosahedronGeometry(1,2);break;
case'torusknot':geo=new THREE.TorusKnotGeometry(1,.3,96,14);break;
default:geo=new THREE.BoxGeometry(1,1,1);
}
const mesh=new THREE.Mesh(geo,mkMat());mesh.castShadow=mesh.receiveShadow=true;
mesh.position.set(pos?pos.x:0,pos?pos.y:(type==='plane'?0:1),pos?pos.z:0);
SCENE.add(mesh);const o={mesh,light:null,name,type:'MESH',vis:true,kf:{}};OBJS.push(o);doSel(o);return o;
}
function mkLight(type,doSel=true){
let light,helper;const nm=type.charAt(0).toUpperCase()+type.slice(1)+' Light';
switch(type){
case'sun':light=new THREE.DirectionalLight(0xfffaf0,1.5);helper=new THREE.DirectionalLightHelper(light,1);break;
case'spot':light=new THREE.SpotLight(0xfffaf0,2,20,Math.PI/5,.3);light.castShadow=true;helper=new THREE.SpotLightHelper(light);break;
default:light=new THREE.PointLight(0xfffaf0,1.5,20);light.castShadow=true;helper=new THREE.PointLightHelper(light,.4);
}
light.position.set((Math.random()-.5)*6,5+Math.random()*3,(Math.random()-.5)*6);SCENE.add(light);
if(helper){helper.position.copy(light.position);SCENE.add(helper);}
const ind=new THREE.Mesh(new THREE.SphereGeometry(.14,8,8),new THREE.MeshBasicMaterial({color:0xffff66,depthTest:false}));
ind.position.copy(light.position);SCENE.add(ind);
const o={mesh:ind,helper,light,name:nm,type:'LIGHT',vis:true,kf:{}};OBJS.push(o);if(doSel)doSel(o);return o;
}
function addObj(t){const n=t.charAt(0).toUpperCase()+t.slice(1)+'.'+(OBJS.filter(o=>o.type==='MESH').length+1);const o=mkMesh(t,n);refreshOL();refreshSt();return o;}
function addLight(t){const o=mkLight(t,true);refreshOL();refreshSt();return o;}
// โ”€โ”€ SELECT โ”€โ”€
function doSel(obj){if(SEL)clrHL(SEL);SEL=obj;if(obj?.type==='MESH')addHL(obj);refreshOL();refreshPr();refreshSt();}
function clrAll(){if(SEL)clrHL(SEL);SEL=null;refreshOL();refreshPr();refreshSt();}
function addHL(o){if(!o.mesh.geometry)return;const m=new THREE.Mesh(o.mesh.geometry,new THREE.MeshBasicMaterial({color:0xff8800,side:THREE.BackSide}));m.name='__hl';m.scale.setScalar(1.06);o.mesh.add(m);}
function clrHL(o){if(!o?.mesh)return;const h=o.mesh.getObjectByName('__hl');if(h)o.mesh.remove(h);}
// โ”€โ”€ POINTER โ”€โ”€
function setupPtr(){
const cv=document.getElementById('c');
cv.addEventListener('mousedown',e=>{PT.dn=true;PT.btn=e.button;PT.drag=false;PT.lx=PT.sx=e.clientX;PT.ly=PT.sy=e.clientY;if(e.button===1)e.preventDefault();});
window.addEventListener('mousemove',e=>{
if(!PT.dn)return;const dx=e.clientX-PT.lx,dy=e.clientY-PT.ly;PT.lx=e.clientX;PT.ly=e.clientY;
if(Math.abs(e.clientX-PT.sx)+Math.abs(e.clientY-PT.sy)>3)PT.drag=true;
if(PT.btn===2||(PT.btn===0&&e.altKey)||PT.btn===1){
if(e.shiftKey||PT.btn===1)doPan(dx,dy);
else{ORB.th-=dx*.008;ORB.ph=clamp(ORB.ph+dy*.008,.05,Math.PI-.05);updCam();}
}
if(PT.btn===0&&!e.altKey&&TOOL!=='select'&&SEL?.type==='MESH'){applyDrag(dx,dy);if(document.getElementById('tlAKF').checked)insKF('all');}
if(MODE==='sculpt'&&PT.btn===0&&SEL?.type==='MESH'&&PT.drag)sculptAt(e.clientX,e.clientY);
});
window.addEventListener('mouseup',e=>{if(!PT.drag&&e.button===0)clickSel(e.clientX,e.clientY,e.shiftKey);PT.dn=false;PT.btn=-1;PT.drag=false;});
cv.addEventListener('wheel',e=>{e.preventDefault();ORB.r=clamp(ORB.r*(e.deltaY>0?1.1:.91),.3,2000);updCam();onResize();},{passive:false});
cv.addEventListener('contextmenu',e=>{e.preventDefault();const ctx=document.getElementById('ctx');ctx.style.left=Math.min(e.clientX,innerWidth-170)+'px';ctx.style.top=Math.min(e.clientY,innerHeight-220)+'px';ctx.classList.add('show');});
cv.addEventListener('dblclick',()=>{if(SEL)act('focus');});
// Touch
let tp=[],tpin=0,tmov=false;
cv.addEventListener('touchstart',e=>{e.preventDefault();tmov=false;tp=Array.from(e.touches).map(t=>({x:t.clientX,y:t.clientY}));if(e.touches.length===2)tpin=Math.hypot(e.touches[1].clientX-e.touches[0].clientX,e.touches[1].clientY-e.touches[0].clientY);},{passive:false});
cv.addEventListener('touchmove',e=>{
e.preventDefault();tmov=true;const cur=Array.from(e.touches).map(t=>({x:t.clientX,y:t.clientY}));
if(cur.length===1&&tp.length){const dx=cur[0].x-tp[0].x,dy=cur[0].y-tp[0].y;
if(MODE==='sculpt'&&SEL?.type==='MESH')sculptAt(cur[0].x,cur[0].y);
else if(TOOL!=='select'&&SEL?.type==='MESH')applyDrag(dx,dy);
else{ORB.th-=dx*.009;ORB.ph=clamp(ORB.ph+dy*.009,.05,Math.PI-.05);updCam();}}
if(cur.length===2&&tp.length>=2){
doPan((cur[0].x+cur[1].x)/2-(tp[0].x+tp[1].x)/2,(cur[0].y+cur[1].y)/2-(tp[0].y+tp[1].y)/2);
const d=Math.hypot(cur[1].x-cur[0].x,cur[1].y-cur[0].y);if(tpin>0){ORB.r=clamp(ORB.r*(tpin/d),.3,2000);updCam();onResize();}tpin=d;}
tp=cur;
},{passive:false});
cv.addEventListener('touchend',e=>{e.preventDefault();if(!tmov&&e.changedTouches.length)clickSel(e.changedTouches[0].clientX,e.changedTouches[0].clientY,false);},{passive:false});
document.addEventListener('click',e=>{if(!e.target.closest('.mi')&&!e.target.closest('.md'))closeMenus();if(!e.target.closest('#ctx'))document.getElementById('ctx').classList.remove('show');});
}
// โ”€โ”€ SCULPT โ”€โ”€
function sculptAt(mx,my){
if(!SEL||SEL.type!=='MESH')return;
const cv=document.getElementById('c'),r=cv.getBoundingClientRect();
const nv=new THREE.Vector2(((mx-r.left)/r.width)*2-1,-((my-r.top)/r.height)*2+1);
RAYCASTER.setFromCamera(nv,getC());
const hits=RAYCASTER.intersectObject(SEL.mesh,false);if(!hits.length)return;
const hp=hits[0].point,pos=SEL.mesh.geometry.attributes.position;
const bR=parseFloat(document.getElementById('sc-sz').value);
const str=parseFloat(document.getElementById('sc-st').value);
const dir=parseInt(document.getElementById('sc-dr').value);
const norm=hits[0].face?hits[0].face.normal.clone().transformDirection(SEL.mesh.matrixWorld):new THREE.Vector3(0,1,0);
for(let i=0;i<pos.count;i++){
const vw=new THREE.Vector3(pos.getX(i),pos.getY(i),pos.getZ(i)).applyMatrix4(SEL.mesh.matrixWorld);
const d=vw.distanceTo(hp);if(d>bR)continue;
const fall=Math.pow(1-d/bR,2)*str*dir;
let nx=pos.getX(i),ny=pos.getY(i),nz=pos.getZ(i);
if(SCULPT==='draw'){nx+=norm.x*fall;ny+=norm.y*fall;nz+=norm.z*fall;}
else if(SCULPT==='inflate'){const out=new THREE.Vector3(nx,ny,nz).normalize();nx+=out.x*fall;ny+=out.y*fall;nz+=out.z*fall;}
else if(SCULPT==='grab'){const t=SEL.mesh.geometry.getAttribute('position');ny+=fall*.5;}
else if(SCULPT==='flatten'){ny+=(0-ny)*Math.abs(fall)*.3;}
else if(SCULPT==='crease'){nx+=norm.x*fall*2.2;ny+=norm.y*fall*2.2;nz+=norm.z*fall*2.2;}
else if(SCULPT==='smooth'){nx*=(1-Math.abs(fall)*.1);ny*=(1-Math.abs(fall)*.05);nz*=(1-Math.abs(fall)*.1);}
pos.setXYZ(i,nx,ny,nz);
}
pos.needsUpdate=true;SEL.mesh.geometry.computeVertexNormals();
}
function subdiv(){
if(!SEL||SEL.type!=='MESH'){toast('Select a mesh first');return;}
const m=SEL.mesh,old=m.geometry;let neo=null;
if(old.type==='BoxGeometry'){const p=old.parameters;neo=new THREE.BoxGeometry(p.width,p.height,p.depth,(p.widthSegments||1)*2,(p.heightSegments||1)*2,(p.depthSegments||1)*2);}
else if(old.type==='SphereGeometry'){const p=old.parameters;neo=new THREE.SphereGeometry(p.radius,Math.min((p.widthSegments||16)*2,64),Math.min((p.heightSegments||12)*2,48));}
else if(old.type==='CylinderGeometry'){const p=old.parameters;neo=new THREE.CylinderGeometry(p.radiusTop,p.radiusBottom,p.height,Math.min((p.radialSegments||16)*2,48),4);}
if(neo){old.dispose();m.geometry=neo;toast('Subdivided: '+neo.attributes.position.count+' verts');}
else toast('Tip: use a higher-poly mesh for best sculpting');
}
function applyDrag(dx,dy){
if(!SEL?.mesh)return;
const spd=ORB.r*.004;
const right=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,0);
const up=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,1);
const m=SEL.mesh;
const ax=TOOL==='grab_x'?'x':TOOL==='grab_y'?'y':TOOL==='grab_z'?'z':null;
if(TOOL==='move'||TOOL.startsWith('grab')){
if(ax==='x')m.position.x+=dx*spd*1.5;else if(ax==='y')m.position.y-=dy*spd*1.5;else if(ax==='z')m.position.z+=dx*spd*1.5;
else{m.position.addScaledVector(right,dx*spd);m.position.addScaledVector(up,-dy*spd);}
if(SEL.light)SEL.light.position.copy(m.position);
}else if(TOOL==='rotate'){
if(ax==='x')m.rotation.x+=dy*.012;else if(ax==='z')m.rotation.z+=dx*.012;else{m.rotation.y+=dx*.012;m.rotation.x+=dy*.006;}
}else if(TOOL==='scale'){
const f=Math.max(.001,1+(dx-dy)*.006);
if(ax==='x')m.scale.x=Math.max(.001,m.scale.x*f);else if(ax==='y')m.scale.y=Math.max(.001,m.scale.y*f);else if(ax==='z')m.scale.z=Math.max(.001,m.scale.z*f);else m.scale.multiplyScalar(f);
}
refreshPV();
}
function doPan(dx,dy){
const spd=ORB.r*.001;
const right=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,0);
const up=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,1);
ORB.tx-=right.x*dx*spd*8;ORB.ty-=right.y*dx*spd*8;ORB.tz-=right.z*dx*spd*8;
ORB.tx+=up.x*dy*spd*8;ORB.ty+=up.y*dy*spd*8;ORB.tz+=up.z*dy*spd*8;updCam();
}
function clickSel(cx,cy,add){
if(MODE!=='object')return;
const cv=document.getElementById('c'),r=cv.getBoundingClientRect();
const nv=new THREE.Vector2(((cx-r.left)/r.width)*2-1,-((cy-r.top)/r.height)*2+1);
RAYCASTER.setFromCamera(nv,getC());
const hits=RAYCASTER.intersectObjects(OBJS.filter(o=>o.vis&&o.mesh).map(o=>o.mesh),true);
if(hits.length){let h=hits[0].object;while(h.parent&&h.parent!==SCENE)h=h.parent;const f=OBJS.find(o=>o.mesh===h);if(f){doSel(f);return;}}
if(!add)clrAll();
}
// โ”€โ”€ KEYS โ”€โ”€
function setupKeys(){
document.addEventListener('keydown',e=>{
const tag=document.activeElement.tagName;
if(tag==='INPUT'||tag==='SELECT'||tag==='TEXTAREA'){if(e.ctrlKey&&e.key==='Enter'){runSc();e.preventDefault();}return;}
if(e.ctrlKey||e.metaKey){switch(e.key){case'z':e.preventDefault();act('undo');break;case'd':e.preventDefault();act('dup');break;case'n':e.preventDefault();act('newScene');break;}return;}
switch(e.key){
case'g':case'G':setTool('move');break;case'r':case'R':setTool('rotate');break;case's':case'S':setTool('scale');break;
case'w':case'W':case'q':case'Q':setTool('select');break;
case'Delete':case'Backspace':act('del');break;
case'a':case'A':SEL?clrAll():(OBJS.length&&doSel(OBJS[0]));break;
case'.':act('focus');break;case'Home':act('viewAll');break;case'F12':e.preventDefault();act('saveRender');break;
case' ':e.preventDefault();tla('play');break;
case'ArrowLeft':e.preventDefault();tla('back');break;case'ArrowRight':e.preventDefault();tla('fwd');break;
case'i':case'I':insKF('all');break;
case'Tab':e.preventDefault();setMode(MODE==='edit'?'object':'edit');break;
case'1':setView('front');break;case'3':setView('side');break;case'7':setView('top');break;case'5':act('toggleOrtho');break;
}
});
}
// โ”€โ”€ ACTIONS โ”€โ”€
function saveU(){UND.push(JSON.stringify(OBJS.map(o=>({name:o.name,p:o.mesh.position.toArray(),r:[o.mesh.rotation.x,o.mesh.rotation.y,o.mesh.rotation.z],s:o.mesh.scale.toArray()}))));if(UND.length>30)UND.shift();}
function act(a){
closeMenus();
switch(a){
case'del':
if(!SEL){toast('Nothing selected');return;}saveU();
if(SEL.light)SCENE.remove(SEL.light);if(SEL.helper)SCENE.remove(SEL.helper);SCENE.remove(SEL.mesh);
OBJS=OBJS.filter(o=>o!==SEL);SEL=null;refreshOL();refreshPr();refreshSt();toast('Deleted');break;
case'dup':
if(!SEL||SEL.type!=='MESH'){toast('Select a mesh');return;}saveU();
const nm2=new THREE.Mesh(SEL.mesh.geometry.clone(),SEL.mesh.material.clone());
nm2.castShadow=nm2.receiveShadow=true;nm2.position.copy(SEL.mesh.position);nm2.position.x+=1.5;nm2.rotation.copy(SEL.mesh.rotation);nm2.scale.copy(SEL.mesh.scale);
SCENE.add(nm2);const no={mesh:nm2,light:null,name:SEL.name+'.copy',type:'MESH',vis:true,kf:{}};OBJS.push(no);doSel(no);refreshOL();refreshSt();toast('Duplicated');break;
case'undo':
if(!UND.length){toast('Nothing to undo');return;}
JSON.parse(UND.pop()).forEach(s=>{const o=OBJS.find(x=>x.name===s.name);if(o){o.mesh.position.fromArray(s.p);o.mesh.rotation.set(...s.r);o.mesh.scale.fromArray(s.s);}});
refreshPV();toast('Undo');break;
case'resetLoc':if(!SEL)return;saveU();SEL.mesh.position.set(0,1,0);refreshPV();toast('Location reset');break;
case'resetRot':if(!SEL)return;saveU();SEL.mesh.rotation.set(0,0,0);refreshPV();toast('Rotation reset');break;
case'resetScale':if(!SEL)return;saveU();SEL.mesh.scale.set(1,1,1);refreshPV();toast('Scale reset');break;
case'mirror_x':if(!SEL)return;saveU();SEL.mesh.scale.x*=-1;refreshPV();toast('Mirrored X');break;
case'snapGround':
if(!SEL||SEL.type!=='MESH')return;saveU();
const bb=new THREE.Box3().setFromObject(SEL.mesh);SEL.mesh.position.y-=bb.min.y;refreshPV();toast('Snapped to ground');break;
case'focus':
if(!SEL)return;const wp=new THREE.Vector3();SEL.mesh.getWorldPosition(wp);ORB.tx=wp.x;ORB.ty=wp.y;ORB.tz=wp.z;ORB.r=5;updCam();toast('Focused');break;
case'viewAll':ORB.tx=0;ORB.ty=1;ORB.tz=0;ORB.r=12;updCam();toast('View All');break;
case'toggleOrtho':ORTHO=!ORTHO;toast(ORTHO?'Orthographic':'Perspective');break;
case'newScene':
if(!confirm('Clear scene?'))return;
OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});
OBJS=[];SEL=null;KF={};buildScene();refreshOL();refreshSt();toast('New Scene');break;
case'saveRender':
const rw=parseInt(document.getElementById('rW').value)||1920,rh=parseInt(document.getElementById('rH').value)||1080;
const fmt=document.getElementById('rFmt').value;
RENDERER.setSize(rw,rh,false);RENDERER.render(SCENE,getC());
const lk=document.createElement('a');lk.download='forge3d.'+(fmt==='PNG'?'png':'jpg');lk.href=RENDERER.domElement.toDataURL(fmt==='PNG'?'image/png':'image/jpeg',.95);lk.click();
onResize();toast('Rendered '+rw+'ร—'+rh);break;
case'exportObj':
let out='# FORGE3D OBJ\n';let vo=1;
OBJS.filter(o=>o.type==='MESH').forEach(o=>{const p=o.mesh.geometry.attributes.position;out+=`o ${o.name}\n`;for(let i=0;i<p.count;i++)out+=`v ${p.getX(i).toFixed(4)} ${p.getY(i).toFixed(4)} ${p.getZ(i).toFixed(4)}\n`;const idx=o.mesh.geometry.index;if(idx)for(let i=0;i<idx.count;i+=3)out+=`f ${idx.getX(i)+vo} ${idx.getX(i+1)+vo} ${idx.getX(i+2)+vo}\n`;vo+=p.count;});
const bl=new Blob([out],{type:'text/plain'});const al=document.createElement('a');al.download='forge3d.obj';al.href=URL.createObjectURL(bl);al.click();toast('OBJ exported');break;
}
}
// โ”€โ”€ MODE / TOOL / SHADING โ”€โ”€
function setMode(m){MODE=m;['object','edit','sculpt'].forEach(mm=>document.getElementById('mb-'+mm)?.classList.toggle('on',mm===m));if(m==='sculpt'){setRP('sc');toast('Sculpt โ€” Paint mesh with LMB');}else if(m==='edit')toast('Edit Mode');else toast('Object Mode');document.getElementById('stm').textContent=' '+m.charAt(0).toUpperCase()+m.slice(1);updBadge();}
function setTool(t){TOOL=t;document.querySelectorAll('.tb[id^="t-"]').forEach(b=>b.classList.remove('on'));document.getElementById('t-'+t)?.classList.add('on');document.getElementById('stt').textContent=' '+t.charAt(0).toUpperCase()+t.slice(1);updBadge();}
function setST(t){SCULPT=t;document.querySelectorAll('.stb').forEach(b=>b.classList.remove('on'));document.getElementById('sc-'+t)?.classList.add('on');}
function setShd(s){
SHADING=s;document.querySelectorAll('.sb').forEach(b=>b.classList.remove('on'));document.getElementById('shd-'+s)?.classList.add('on');
OBJS.forEach(o=>{if(!o.mesh.material)return;if(s==='wire'){o.mesh.material.wireframe=true;o.mesh.material.transparent=false;}
else if(s==='xray'){o.mesh.material.wireframe=false;o.mesh.material.transparent=true;o.mesh.material.opacity=.25;}
else{o.mesh.material.wireframe=false;o.mesh.material.transparent=(o.mesh.material.opacity??1)<1;o.mesh.material.opacity=o.mesh.material.opacity??1;}});
}
function setView(v){if(v==='front'){ORB.th=0;ORB.ph=Math.PI/2;}else if(v==='side'){ORB.th=Math.PI/2;ORB.ph=Math.PI/2;}else{ORB.th=0;ORB.ph=.01;}ORB.r=12;updCam();toast(v+' view');}
function toggleRP(){rpOpen=!rpOpen;document.getElementById('rp').classList.toggle('hid',!rpOpen);}
function toggleTL(){tlVis=!tlVis;document.getElementById('tlw').style.display=tlVis?'flex':'none';}
function switchTab(t){['vp','sc','sh'].forEach(tt=>{document.getElementById('tabt-'+tt)?.classList.toggle('on',tt===t);document.getElementById('tc-'+tt)?.classList.toggle('on',tt===t);});if(t==='vp')setTimeout(onResize,50);}
function openMenu(btn,id){closeMenus();const d=document.getElementById(id);if(!d)return;const r=btn.getBoundingClientRect();d.style.left=r.left+'px';d.classList.add('show');btn.classList.add('open');}
function closeMenus(){document.querySelectorAll('.md').forEach(d=>d.classList.remove('show'));document.querySelectorAll('.mb').forEach(b=>b.classList.remove('open'));}
function showHelp(){alert('FORGE3D Shortcuts:\nW=Select G=Move R=Rotate S=Scale\nDel=Delete Ctrl+D=Duplicate Ctrl+Z=Undo\nA=Sel All .=Focus Home=View All\n1=Front 3=Side 7=Top 5=Ortho\nSpace=Play โ†โ†’=Frames I=Insert KF\nTab=Edit Mode F12=Render\nCtrl+Enter(Script)=Run');}
function updBadge(){const b=document.getElementById('vbadge');if(b)b.textContent=MODE.charAt(0).toUpperCase()+MODE.slice(1)+' ยท '+TOOL+(ORTHO?' [Ortho]':'')+' ยท RMB=Orbit ยท Scroll=Zoom ยท Shift+RMB=Pan';}
// โ”€โ”€ OUTLINER โ”€โ”€
function refreshOL(){
const list=document.getElementById('ollist');if(!list)return;list.innerHTML='';
OBJS.forEach(o=>{
const d=document.createElement('div');d.className='oli'+(SEL===o?' sel':'');
const ico=o.type==='LIGHT'?'๐Ÿ’ก':'โ–ฃ';
d.innerHTML=`<span>${ico}</span><span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;">${o.name}</span><span class="eye" onclick="event.stopPropagation();togVis('${o.name}')">${o.vis?'๐Ÿ‘':'โ—‹'}</span>`;
d.addEventListener('click',()=>doSel(o));list.appendChild(d);
});
}
function filterOL(q){document.querySelectorAll('.oli').forEach(el=>{el.style.display=el.textContent.toLowerCase().includes(q.toLowerCase())?'':'none';});}
function togVis(name){const o=OBJS.find(x=>x.name===name);if(!o)return;o.vis=!o.vis;o.mesh.visible=o.vis;if(o.light)o.light.visible=o.vis;refreshOL();}
// โ”€โ”€ PROPS โ”€โ”€
function refreshPr(){
const show=!!SEL;document.getElementById('pno').style.display=show?'none':'';document.getElementById('pbd').style.display=show?'':'none';
document.getElementById('mno').style.display=(show&&SEL.type==='MESH')?'none':'';document.getElementById('mbd').style.display=(show&&SEL.type==='MESH')?'':'none';
if(show)refreshPV();
}
function refreshPV(){
if(!SEL)return;const m=SEL.mesh,g=id=>document.getElementById(id);
g('pNm')&&(g('pNm').value=SEL.name);g('pTy')&&(g('pTy').value=SEL.type);
g('pLX')&&(g('pLX').value=m.position.x.toFixed(3));g('pLY')&&(g('pLY').value=m.position.y.toFixed(3));g('pLZ')&&(g('pLZ').value=m.position.z.toFixed(3));
g('pRX')&&(g('pRX').value=(m.rotation.x*R2D).toFixed(1));g('pRY')&&(g('pRY').value=(m.rotation.y*R2D).toFixed(1));g('pRZ')&&(g('pRZ').value=(m.rotation.z*R2D).toFixed(1));
g('pSX')&&(g('pSX').value=m.scale.x.toFixed(3));g('pSY')&&(g('pSY').value=m.scale.y.toFixed(3));g('pSZ')&&(g('pSZ').value=m.scale.z.toFixed(3));
if(SEL.type==='MESH'&&m.material){g('pCol')&&(g('pCol').value='#'+(m.material.color.getHexString?.()??'888888'));g('pMet')&&(g('pMet').value=m.material.metalness||0);g('pRgh')&&(g('pRgh').value=m.material.roughness||.75);g('pEmt')&&(g('pEmt').value=m.material.emissiveIntensity||0);g('pAlp')&&(g('pAlp').value=m.material.opacity??1);}
}
function setProp(p,v){
if(!SEL)return;const m=SEL.mesh;
switch(p){
case'name':SEL.name=v;refreshOL();break;
case'lx':m.position.x=v;break;case'ly':m.position.y=v;break;case'lz':m.position.z=v;break;
case'rx':m.rotation.x=v*D2R;break;case'ry':m.rotation.y=v*D2R;break;case'rz':m.rotation.z=v*D2R;break;
case'sx':m.scale.x=v;break;case'sy':m.scale.y=v;break;case'sz':m.scale.z=v;break;
case'color':m.material?.color.set(v);break;
case'metal':if(m.material)m.material.metalness=v;break;
case'rough':if(m.material)m.material.roughness=v;break;
case'emit':if(m.material){m.material.emissiveIntensity=v;if(v>0&&m.material.emissive)m.material.emissive.setHex(0xffffff);}break;
case'alpha':if(m.material){m.material.opacity=v;m.material.transparent=v<1;}break;
}
if(SEL.light&&['lx','ly','lz'].includes(p))SEL.light.position.copy(m.position);
}
function applyPre(p){
if(!SEL||SEL.type!=='MESH')return;const mat=SEL.mesh.material;if(!mat)return;
mat.wireframe=false;mat.transparent=false;mat.opacity=1;if(mat.emissive)mat.emissive.setHex(0);mat.emissiveIntensity=0;
const PR={chrome:{c:'#d4d4d4',m:.95,r:.05},gold:{c:'#FFD700',m:.9,r:.1},rubber:{c:'#1a1a1a',m:0,r:1},
glass:{c:'#aaddff',m:0,r:0,tr:true,op:.2},neon:{c:'#00ffff',m:0,r:1,e:'#00ffff',ei:4},
emit:{c:'#ff8800',m:0,r:1,e:'#ff8800',ei:3},lava:{c:'#331100',m:0,r:1,e:'#ff4400',ei:1},
ice:{c:'#aaddff',m:.1,r:.05,tr:true,op:.7},steel:{c:'#445566',m:.8,r:.3},plasma:{c:'#ffffff',m:0,r:1,e:'#8800ff',ei:5}};
const pr=PR[p];if(!pr)return;
mat.color.set(pr.c);mat.metalness=pr.m;mat.roughness=pr.r;
if(pr.tr){mat.transparent=true;mat.opacity=pr.op;}
if(pr.e&&mat.emissive){mat.emissive.set(pr.e);mat.emissiveIntensity=pr.ei;}
mat.needsUpdate=true;refreshPV();toast('Preset: '+p);
}
function applyTexF(input){
const file=input.files[0];if(!file||!SEL||SEL.type!=='MESH')return;
const url=URL.createObjectURL(file);
new THREE.TextureLoader().load(url,tex=>{tex.wrapS=tex.wrapT=THREE.RepeatWrapping;SEL.mesh.material.map=tex;SEL.mesh.material.needsUpdate=true;toast('Texture applied');});
input.value='';
}
function toggleSec(id){const el=document.getElementById(id);if(el)el.style.display=el.style.display==='none'?'':'none';}
function setRP(t){['ol','pr','mat','ren','sc'].forEach(tt=>{document.getElementById('rpt-'+tt)?.classList.toggle('on',tt===t);document.getElementById('rpb-'+tt)?.classList.toggle('on',tt===t);});}
function setRP2(v){const[w,h]=v.split(',');document.getElementById('rW').value=w;document.getElementById('rH').value=h;}
function refreshSt(){document.getElementById('sto').textContent=' '+OBJS.length;if(SEL?.mesh?.geometry?.attributes?.position)document.getElementById('stv').textContent=' '+SEL.mesh.geometry.attributes.position.count;else document.getElementById('stv').textContent=' 0';}
// โ”€โ”€ GIZMO โ”€โ”€
function drawGiz(){
const cv=document.getElementById('giz');if(!cv||!CAM)return;
const ctx=cv.getContext('2d'),sz=68,cx=34,cy=34,r=24;ctx.clearRect(0,0,sz,sz);
ctx.fillStyle='rgba(0,0,0,.45)';ctx.beginPath();ctx.arc(cx,cy,r+5,0,Math.PI*2);ctx.fill();
const right=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,0),up=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,1),dir=new THREE.Vector3();CAM.getWorldDirection(dir);
[{n:'X',c:'#e44',v:new THREE.Vector3(1,0,0)},{n:'Y',c:'#4e4',v:new THREE.Vector3(0,1,0)},{n:'Z',c:'#55f',v:new THREE.Vector3(0,0,1)},
{n:'',c:'#622',v:new THREE.Vector3(-1,0,0)},{n:'',c:'#262',v:new THREE.Vector3(0,-1,0)},{n:'',c:'#226',v:new THREE.Vector3(0,0,-1)}]
.sort((a,b)=>a.v.dot(dir)-b.v.dot(dir)).forEach(ax=>{
const px=ax.v.dot(right)*r,py=-ax.v.dot(up)*r;const neg=!ax.n;
ctx.globalAlpha=neg?.28:.9;ctx.strokeStyle=ax.c;ctx.lineWidth=neg?1:2;
ctx.beginPath();ctx.moveTo(cx,cy);ctx.lineTo(cx+px,cy+py);ctx.stroke();
if(!neg){ctx.fillStyle=ax.c;ctx.beginPath();ctx.arc(cx+px,cy+py,5,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.font='bold 7px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(ax.n,cx+px,cy+py);}
});ctx.globalAlpha=1;
}
// โ”€โ”€ TIMELINE โ”€โ”€
function setupTL(){const cv=document.getElementById('tlcv');if(!cv)return;cv.addEventListener('click',e=>{const r=cv.getBoundingClientRect();setFr(Math.max(1,Math.round((e.clientX-r.left)/cv.offsetWidth*(FR_END-1)+1)));});}
function setupTLR(){const h=document.getElementById('tlr');if(!h)return;let sy,sh;h.addEventListener('mousedown',e=>{sy=e.clientY;sh=document.getElementById('tlw').offsetHeight;});window.addEventListener('mousemove',e=>{if(sy===undefined)return;document.getElementById('tlw').style.height=Math.max(55,Math.min(250,sh-(e.clientY-sy)))+'px';});window.addEventListener('mouseup',()=>{sy=undefined;});}
function tla(a){switch(a){case'first':setFr(1);break;case'back':setFr(Math.max(1,FR-1));break;case'play':TL_PLAY=!TL_PLAY;document.getElementById('tlplay').textContent=TL_PLAY?'โธ':'โ–ถ';if(TL_PLAY)playL();break;case'fwd':setFr(Math.min(FR_END,FR+1));break;case'last':setFr(FR_END);break;}}
function playL(){if(!TL_PLAY)return;FR++;if(FR>FR_END)FR=1;setFr(FR);setTimeout(playL,1000/FR_FPS);}
function setFr(f){FR=clamp(f,1,FR_END);document.getElementById('tlf').value=FR;applyKF();drawTL();}
function insKF(ch){
if(!SEL)return;const nm=SEL.name,m=SEL.mesh;if(!KF[nm])KF[nm]={};
const v={lx:m.position.x,ly:m.position.y,lz:m.position.z,rx:m.rotation.x,ry:m.rotation.y,rz:m.rotation.z,sx:m.scale.x,sy:m.scale.y,sz:m.scale.z};
const s={loc:['lx','ly','lz'],rot:['rx','ry','rz'],scl:['sx','sy','sz'],all:Object.keys(v)};
(s[ch]||[ch]).forEach(c=>{if(!KF[nm][c])KF[nm][c]={};KF[nm][c][FR]=v[c];});drawTL();toast('KF at frame '+FR);
}
function delKF(){if(!SEL)return;const nm=SEL.name;if(!KF[nm])return;Object.values(KF[nm]).forEach(c=>{delete c[FR];});drawTL();toast('KF deleted');}
function applyKF(){
if(!SEL)return;const nm=SEL.name;if(!KF[nm])return;const m=SEL.mesh;
function lv(ch){const k=KF[nm][ch];if(!k)return null;const fr=Object.keys(k).map(Number).sort((a,b)=>a-b);if(!fr.length)return null;if(FR<=fr[0])return k[fr[0]];if(FR>=fr[fr.length-1])return k[fr[fr.length-1]];for(let i=0;i<fr.length-1;i++){if(FR>=fr[i]&&FR<=fr[i+1]){const t=(FR-fr[i])/(fr[i+1]-fr[i]);return k[fr[i]]+(k[fr[i+1]]-k[fr[i]])*t;}}return null;}
['lx','ly','lz'].forEach((c,i)=>{const v=lv(c);if(v!==null)m.position.setComponent(i,v);});
const rx=lv('rx'),ry=lv('ry'),rz=lv('rz');if(rx!==null)m.rotation.x=rx;if(ry!==null)m.rotation.y=ry;if(rz!==null)m.rotation.z=rz;
['sx','sy','sz'].forEach((c,i)=>{const v=lv(c);if(v!==null)m.scale.setComponent(i,v);});
if(SEL.light)SEL.light.position.copy(m.position);refreshPV();
}
function drawTL(){
const cv=document.getElementById('tlcv');if(!cv)return;const w=cv.offsetWidth,h=cv.offsetHeight;cv.width=w;cv.height=h;if(!w||!h)return;
const ctx=cv.getContext('2d');ctx.fillStyle='#0d0d0d';ctx.fillRect(0,0,w,h);
const step=Math.max(1,Math.floor(FR_END/20));for(let f=0;f<=FR_END;f+=step){const x=(f/(FR_END-1))*w;ctx.strokeStyle='#222';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke();}
if(SEL){const kfs=KF[SEL.name]||{};const cols={lx:'#e44',ly:'#4e4',lz:'#66f',rx:'#ea4',ry:'#4ea',rz:'#a4e'};const chs=['lx','ly','lz','rx','ry','rz'];
chs.forEach((ch,ci)=>{if(!kfs[ch])return;const cy=((ci+.5)/chs.length)*h;Object.keys(kfs[ch]).forEach(f=>{const x=((f-1)/(FR_END-1))*w;ctx.fillStyle=cols[ch]||'#fff';ctx.beginPath();ctx.moveTo(x,cy-5);ctx.lineTo(x+4,cy);ctx.lineTo(x,cy+5);ctx.lineTo(x-4,cy);ctx.closePath();ctx.fill();});});}
const px=((FR-1)/(Math.max(1,FR_END-1)))*w;ctx.strokeStyle='#e87d39';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(px,0);ctx.lineTo(px,h);ctx.stroke();ctx.fillStyle='#e87d39';ctx.beginPath();ctx.moveTo(px-5,0);ctx.lineTo(px+5,0);ctx.lineTo(px,9);ctx.closePath();ctx.fill();
}
// โ”€โ”€ SCRIPT โ”€โ”€
function runSc(){
const code=document.getElementById('sa').value,out=document.getElementById('so');out.textContent='';
const ol=console.log;console.log=(...a)=>{out.textContent+=a.join(' ')+'\n';ol(...a);};
try{new Function('SCENE','OBJS','CAM','KF','FR_FPS','FR_END','addObj','addLight','act','toast','THREE','drawTL','refreshOL','refreshSt','applyPre','SEL','applyKF','setFr',code)(SCENE,OBJS,CAM,KF,FR_FPS,FR_END,addObj,addLight,act,toast,THREE,drawTL,refreshOL,refreshSt,applyPre,SEL,applyKF,setFr);refreshOL();refreshSt();if(!out.textContent)out.textContent='// Done โœ“';}
catch(e){out.textContent='โŒ '+e.message;}
console.log=ol;
}
function clrSc(){document.getElementById('sa').value='';document.getElementById('so').textContent='// Cleared';}
function insTpl(t){
const T={
cubes:`for(let i=0;i<8;i++){const o=addObj('cube');o.mesh.position.set((Math.random()-.5)*12,Math.random()*4+.5,(Math.random()-.5)*12);o.mesh.material.color.setHSL(i/8,.75,.5);o.mesh.material.metalness=Math.random();o.mesh.material.roughness=Math.random();o.mesh.scale.setScalar(.5+Math.random()*1.5);}toast('8 cubes!');`,
solar:`// Clear scene
OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});OBJS.length=0;
const sun=new THREE.Mesh(new THREE.SphereGeometry(2.5,32,32),new THREE.MeshStandardMaterial({color:'#FDB813',emissive:'#FDB813',emissiveIntensity:2,roughness:1,metalness:0}));
SCENE.add(sun);OBJS.push({mesh:sun,light:null,name:'Sun',type:'MESH',vis:true,kf:{}});
SCENE.add(new THREE.PointLight(0xfff4e0,4,300));SCENE.add(new THREE.AmbientLight(0x111133,.5));
const pd=[['Mercury','#b5b5b5',.3,5,4.7],['Venus','#e8cda0',.5,8,3.5],['Earth','#3b7fc4',.6,11,3.0],['Mars','#c1440e',.38,15,2.4],['Jupiter','#c88b3a',1.2,22,1.3],['Saturn','#e4d191',1.0,30,1.0],['Uranus','#7de8e8',.7,38,.7],['Neptune','#4b70dd',.65,45,.5]];
const planets=[];
pd.forEach(([name,col,r,orb,spd])=>{
const m=new THREE.Mesh(new THREE.SphereGeometry(r,28,28),new THREE.MeshStandardMaterial({color:col,roughness:.8,metalness:.1}));
m.castShadow=true;m.position.x=orb;SCENE.add(m);OBJS.push({mesh:m,light:null,name,type:'MESH',vis:true,kf:{}});
const ring=new THREE.Mesh(new THREE.RingGeometry(orb-.04,orb+.04,64),new THREE.MeshBasicMaterial({color:0x444444,side:THREE.DoubleSide}));
ring.rotation.x=-Math.PI/2;SCENE.add(ring);
if(name==='Saturn'){const rm=new THREE.Mesh(new THREE.RingGeometry(r+.5,r+1.2,64),new THREE.MeshStandardMaterial({color:'#d4c080',side:THREE.DoubleSide,transparent:true,opacity:.7}));rm.rotation.x=Math.PI/3;m.add(rm);}
planets.push({mesh:m,orb,spd:spd*.22,ang:Math.random()*Math.PI*2});
});
const t0=Date.now();
(function animSolar(){requestAnimationFrame(animSolar);const t=(Date.now()-t0)*.001;planets.forEach(p=>{p.mesh.position.x=Math.cos(p.ang+t*p.spd)*p.orb;p.mesh.position.z=Math.sin(p.ang+t*p.spd)*p.orb;p.mesh.rotation.y+=.008;});sun.rotation.y+=.003;})();
refreshOL();toast('Animated Solar System!');`,
blackhole:`// Clear scene
OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});OBJS.length=0;
// Core
const core=new THREE.Mesh(new THREE.SphereGeometry(2.2,32,32),new THREE.MeshStandardMaterial({color:'#000000',emissive:'#220033',emissiveIntensity:.5,roughness:1,metalness:0}));
SCENE.add(core);OBJS.push({mesh:core,light:null,name:'BH_Core',type:'MESH',vis:true,kf:{}});
// 5 Accretion disks at varied angles - THIS is the key: each disk at different rotation
const diskData=[[3,5.5,'#ff6600',3,.75,0],[5.5,7.5,'#ff9900',2.5,.65,.3],[7.5,9.5,'#ffcc00',2,.55,.6],[9.5,11.5,'#ff3300',1.5,.45,.9],[12,14.5,'#882200',.9,.35,1.1]];
diskData.forEach(([i,o,col,ei,op,xrot],idx)=>{
const geo=new THREE.RingGeometry(i,o,128);
const mat=new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:ei,transparent:true,opacity:op,side:THREE.DoubleSide,roughness:1,metalness:0});
const ring=new THREE.Mesh(geo,mat);
// KEY: rotate each disk differently so they're visible from any angle
ring.rotation.x=Math.PI/2+xrot*.3;ring.rotation.z=xrot*.2;ring.rotation.y=xrot*.15;
SCENE.add(ring);OBJS.push({mesh:ring,light:null,name:'Disk'+idx,type:'MESH',vis:true,kf:{}});
});
// Photon ring
const photon=new THREE.Mesh(new THREE.TorusGeometry(2.5,.1,16,128),new THREE.MeshStandardMaterial({color:'#ffffff',emissive:'#ffffff',emissiveIntensity:6,roughness:1,metalness:0}));
SCENE.add(photon);OBJS.push({mesh:photon,light:null,name:'PhotonRing',type:'MESH',vis:true,kf:{}});
// Polar jets
[[1,'up'],[-1,'down']].forEach(([dir,nm])=>{
const pts=[new THREE.Vector3(0,0,0),new THREE.Vector3(0.3*dir,5*dir,0),new THREE.Vector3(0,14*dir,0)];
const curve=new THREE.CatmullRomCurve3(pts);
const jet=new THREE.Mesh(new THREE.TubeGeometry(curve,32,.2,8,false),new THREE.MeshStandardMaterial({color:'#2244ff',emissive:'#2244ff',emissiveIntensity:4,transparent:true,opacity:.7,roughness:1,metalness:0}));
SCENE.add(jet);OBJS.push({mesh:jet,light:null,name:'Jet_'+nm,type:'MESH',vis:true,kf:{}});
});
// Particle swirl
const pg=new THREE.BufferGeometry();const pp=new Float32Array(200*3);
for(let i=0;i<200;i++){const a=Math.random()*Math.PI*2,r=2.5+Math.random()*12,h=(Math.random()-.5)*2;pp[i*3]=Math.cos(a)*r;pp[i*3+1]=h;pp[i*3+2]=Math.sin(a)*r;}
pg.setAttribute('position',new THREE.Float32BufferAttribute(pp,3));
const stars=new THREE.Points(pg,new THREE.PointsMaterial({color:'#ffaa44',size:.15,sizeAttenuation:true}));
SCENE.add(stars);OBJS.push({mesh:stars,light:null,name:'Particles',type:'MESH',vis:true,kf:{}});
// Lighting
const pl=new THREE.PointLight(0xff6600,4,60);SCENE.add(pl);SCENE.add(new THREE.AmbientLight(0x111133,.6));
// Animate
(function animBH(){requestAnimationFrame(animBH);core.rotation.y+=.005;stars.rotation.y+=.004;photon.rotation.z+=.025;})();
refreshOL();toast('Black Hole โ€” animated with jets, particles, 5 disks!');`,
missile:`// Clear scene
OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});OBJS.length=0;
const dS=new THREE.MeshStandardMaterial({color:'#2a3344',metalness:.8,roughness:.3});
const cH=new THREE.MeshStandardMaterial({color:'#d4d4d4',metalness:.95,roughness:.05});
const orG=new THREE.MeshStandardMaterial({color:'#ff6600',emissive:'#ff6600',emissiveIntensity:2.5,roughness:1,metalness:0});
// Body
const pts=[new THREE.Vector2(0,0),new THREE.Vector2(.45,.4),new THREE.Vector2(.55,1.2),new THREE.Vector2(.57,2.5),new THREE.Vector2(.54,4),new THREE.Vector2(.45,5),new THREE.Vector2(.28,5.8),new THREE.Vector2(.1,6.2),new THREE.Vector2(0,6.6)];
const body=new THREE.Mesh(new THREE.LatheGeometry(pts,32),dS.clone());body.castShadow=true;body.position.y=1;SCENE.add(body);OBJS.push({mesh:body,light:null,name:'Body',type:'MESH',vis:true,kf:{}});
// Nose
const nose=new THREE.Mesh(new THREE.ConeGeometry(.1,1.2,20),cH.clone());nose.position.y=7.8;nose.castShadow=true;SCENE.add(nose);OBJS.push({mesh:nose,light:null,name:'Nose',type:'MESH',vis:true,kf:{}});
// 4 Fins
for(let i=0;i<4;i++){
const sh=new THREE.Shape();sh.moveTo(0,0);sh.lineTo(.95,0);sh.lineTo(0,2.2);sh.closePath();
const fin=new THREE.Mesh(new THREE.ExtrudeGeometry(sh,{depth:.05,bevelEnabled:true,bevelSize:.02,bevelSegments:1}),dS.clone());
fin.castShadow=true;const a=i*Math.PI/2;fin.position.set(Math.cos(a)*.56,1.1,Math.sin(a)*.56);fin.rotation.y=-a;SCENE.add(fin);OBJS.push({mesh:fin,light:null,name:'Fin'+i,type:'MESH',vis:true,kf:{}});
}
// Nozzle + glow
const noz=new THREE.Mesh(new THREE.CylinderGeometry(.52,.34,.65,24),new THREE.MeshStandardMaterial({color:'#111',metalness:.9,roughness:.1}));noz.position.y=.68;SCENE.add(noz);OBJS.push({mesh:noz,light:null,name:'Nozzle',type:'MESH',vis:true,kf:{}});
const exh=new THREE.Mesh(new THREE.CylinderGeometry(.28,.48,.9,24),orG.clone());exh.position.y=.1;SCENE.add(exh);OBJS.push({mesh:exh,light:null,name:'Exhaust',type:'MESH',vis:true,kf:{}});
const gring=new THREE.Mesh(new THREE.TorusGeometry(.4,.07,10,32),orG.clone());gring.position.y=.48;gring.rotation.x=Math.PI/2;SCENE.add(gring);OBJS.push({mesh:gring,light:null,name:'GlowRing',type:'MESH',vis:true,kf:{}});
// Warhead band
const band=new THREE.Mesh(new THREE.CylinderGeometry(.59,.59,.2,32),new THREE.MeshStandardMaterial({color:'#cc1111',metalness:.7,roughness:.2}));band.position.y=6;SCENE.add(band);OBJS.push({mesh:band,light:null,name:'Warhead',type:'MESH',vis:true,kf:{}});
// 4 fuel tubes
for(let i=0;i<4;i++){const a=i*Math.PI/2+Math.PI/4;const curve=new THREE.CatmullRomCurve3([new THREE.Vector3(Math.cos(a)*.62,1.3,Math.sin(a)*.62),new THREE.Vector3(Math.cos(a)*.60,3.5,Math.sin(a)*.60),new THREE.Vector3(Math.cos(a)*.56,5.5,Math.sin(a)*.56)]);const tube=new THREE.Mesh(new THREE.TubeGeometry(curve,32,.04,8,false),new THREE.MeshStandardMaterial({color:'#556677',metalness:.6,roughness:.4}));tube.castShadow=true;SCENE.add(tube);OBJS.push({mesh:tube,light:null,name:'Tube'+i,type:'MESH',vis:true,kf:{}});}
// Rivets
for(let i=0;i<16;i++){const a=i*Math.PI/8;const rv=new THREE.Mesh(new THREE.SphereGeometry(.04,6,6),cH.clone());rv.position.set(Math.cos(a)*.59,2.5,Math.sin(a)*.59);SCENE.add(rv);OBJS.push({mesh:rv,light:null,name:'Rivet'+i,type:'MESH',vis:true,kf:{}});}
// Lights
const pl=new THREE.PointLight(0xff6600,3,20);pl.position.set(0,.5,0);SCENE.add(pl);SCENE.add(new THREE.AmbientLight(0x334455,.7));SCENE.add(new THREE.DirectionalLight(0xffffff,1));
refreshOL();toast('Missile โ€” 15+ parts, LatheGeometry, fins, tubes, glow!');`
};
if(T[t])document.getElementById('sa').value=T[t];
}
// โ”€โ”€ RENDER LOOP โ”€โ”€
function loop(){requestAnimationFrame(loop);_fc++;const n=performance.now();if(n-_fl>=1000){document.getElementById('stfps').textContent='FPS:'+_fc;_fc=0;_fl=n;}RENDERER.render(SCENE,getC());}
function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
let _tt;function toast(msg,dur=3000){const el=document.getElementById('toast');if(!el)return;el.textContent=msg;el.style.display='block';clearTimeout(_tt);_tt=setTimeout(()=>el.style.display='none',dur);}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// AI PANEL
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
function toggleAI(){AI_OPEN=!AI_OPEN;document.getElementById('aip').classList.toggle('open',AI_OPEN);}
function setAM(m){AI_M=m;document.querySelectorAll('.amt').forEach(b=>b.classList.remove('on'));document.getElementById('at-'+m)?.classList.add('on');}
function togTex(){const d=document.getElementById('tdlg');d.classList.toggle('show');document.getElementById('aaf').classList.remove('show');if(d.classList.contains('show'))document.getElementById('ti').focus();}
function togAF(){const f=document.getElementById('aaf');f.classList.toggle('show');document.getElementById('tdlg').classList.remove('show');}
async function chkStatus(){
const st=document.getElementById('aist');st.textContent='Checkingโ€ฆ';
try{
const r=await fetch('/api/status');if(!r.ok)throw new Error(r.status);
const data=await r.json();const ok=Object.values(data).filter(v=>String(v).startsWith('โœ…')).length;
st.textContent=ok+' ready';st.style.background=ok>0?'rgba(0,128,0,.4)':'rgba(128,0,0,.4)';
addMsg('๐Ÿ“ก Status:\n'+Object.values(data).join('\n'),'ai','g');
}catch(e){st.textContent='โš  Offline';st.style.background='rgba(128,0,0,.4)';addMsg('โŒ Server offline โ€” is app.py running?','ai','e');}
}
function quickAsk(msg){document.getElementById('aii').value=msg;sendAI();}
function qTex(q){document.getElementById('ti').value=q;sendTex();}
function addMsg(text,role,model){
const msgs=document.getElementById('ams'),d=document.createElement('div');
d.className=role==='user'?'amu':('ama '+(model||''));d.textContent=text;msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;return d;
}
function showTh(){const msgs=document.getElementById('ams'),d=document.createElement('div');d.className='ama';d.id='ai-th';d.innerHTML='<div class="ath"><div class="ad"></div><div class="ad"></div><div class="ad"></div></div>';msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;}
function rmTh(){document.getElementById('ai-th')?.remove();}
async function sendAI(){
const inp=document.getElementById('aii'),msg=inp.value.trim();if(!msg)return;
// โ”€โ”€ INSTANT DELETE โ”€โ”€
const low=msg.toLowerCase();
if(/delete\s+(everything|all|scene)|clear\s+(scene|all|everything)|remove\s+all/.test(low)){
inp.value='';addMsg(msg,'user');
OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});
OBJS.length=0;SEL=null;refreshOL();refreshSt();toast('Scene cleared');
addMsg('๐Ÿ—‘๏ธ Scene cleared!','ai','g');return;
}
const delM=low.match(/^(?:delete|remove)\s+(?:the\s+)?(.+)$/);
if(delM){const found=OBJS.find(o=>o.name.toLowerCase().includes(delM[1].trim()));
if(found){inp.value='';addMsg(msg,'user');doSel(found);act('del');addMsg(`๐Ÿ—‘๏ธ Deleted "${found.name}"!`,'ai','g');return;}
}
// โ”€โ”€ AI IMAGE GEN via Pollinations โ”€โ”€
if(/^(?:generate|create|make|show)\s+(?:an?\s+)?image\s+of\s+(.+)$/i.test(msg)){
const imgP=msg.match(/(?:image\s+of\s+)(.+)/i)[1];
inp.value='';addMsg(msg,'user');showTh();
const ir=await fetch('/api/image-gen',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt:imgP,w:512,h:512})});
const id=await ir.json();rmTh();
addMsg(`๐Ÿ–ผ๏ธ Image: <img src="${id.url}" style="max-width:200px;border-radius:8px;margin-top:4px" onerror="this.style.display='none'"><br><small>${id.prompt}</small>`,'ai','g');
return;
}
inp.value='';document.getElementById('ais').disabled=true;addMsg(msg,'user');showTh();
const model=AI_M;if(!AI_H[model])AI_H[model]=[];
const objCtx=OBJS.length?OBJS.slice(0,8).map(o=>`"${o.name}"[${o.mesh.position.x.toFixed(1)},${o.mesh.position.y.toFixed(1)},${o.mesh.position.z.toFixed(1)}]`).join(','):'empty';
const enhanced=`Build: ${msg}\nScene has: ${objCtx}. Selected: ${SEL?.name||'none'}.\nRULES:\n1. Raw JS only โ€” no markdown no backticks\n2. NEVER redeclare SCENE THREE OBJS CAM (already exist = crash)\n3. SCENE.add(mesh) BEFORE OBJS.push โ€” without it mesh is INVISIBLE\n4. Animations: use requestAnimationFrame loop with position/rotation changes\n5. Cars/vehicles that move: use RAF loop changing position each frame\n6. All materials: MeshStandardMaterial with metalness+roughness\n7. End with toast()`;
try{
const res=await fetch('/api/'+model,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:[...AI_H[model],{role:'user',content:enhanced}],max_tokens:3500,temperature:.15})});
const data=await res.json();rmTh();
if(data.error){addMsg('โŒ '+data.error,'ai','e');document.getElementById('ais').disabled=false;return;}
let code=(data.choices?.[0]?.message?.content||'').trim().replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim();
if(!code){addMsg('โš  Empty โ€” try again or switch model','ai','e');document.getElementById('ais').disabled=false;return;}
// Safety: strip redeclarations of globals
code=code.replace(/\b(const|let|var)\s+(SCENE|THREE|OBJS|CAM)\s*=/g,'/* $2 already defined */ //');
AI_H[model]=[...AI_H[model],{role:'user',content:msg},{role:'assistant',content:code.substring(0,400)}].slice(-8);
const lbl={groq:'โšก Groq',nemotron:'๐ŸŸข Nemotron',openrouter:'๐Ÿ”ฎ Detail AI'};
const used=data._fallback?'โšก Groq(fallback)':(data._model_used?data._model_used:lbl[model]);
document.getElementById('sa').value='// '+used+' โ€” '+new Date().toLocaleTimeString()+'\n'+code;
const pre=document.getElementById('apre');pre.textContent=code.substring(0,180)+(code.length>180?'โ€ฆ':'');pre.style.display='block';setTimeout(()=>pre.style.display='none',6000);
try{
new Function('SCENE','OBJS','CAM','KF','FR_FPS','FR_END','addObj','addLight','act','toast','THREE','drawTL','refreshOL','refreshSt','applyPre','SEL','applyKF','setFr',
code)(SCENE,OBJS,CAM,KF,FR_FPS,FR_END,addObj,addLight,act,toast,THREE,drawTL,refreshOL,refreshSt,applyPre,SEL,applyKF,setFr);
refreshOL();refreshSt();addMsg('โœ… Done ('+used+')\nโ†’ Code in ๐Ÿ“ Script tab','ai',model==='groq'?'g':model==='nemotron'?'n':'o');
}catch(e){addMsg('โš  JS Error: '+e.message+'\nโ†’ Check ๐Ÿ“ Script tab','ai','e');}
}catch(e){rmTh();addMsg('โŒ Network: '+e.message,'ai','e');}
document.getElementById('ais').disabled=false;
}
async function genAITexture(){
if(!SEL){toast('Select an object first');return;}
const desc=prompt(`AI texture for "${SEL.name}" โ€” describe it:`,`${SEL.name} surface texture`);
if(!desc)return;
addMsg(`๐ŸŽจ Generating AI texture: ${desc}`,'user');showTh();
const r=await fetch('/api/texture-gen',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({description:desc,object:SEL.name})});
const d=await r.json();rmTh();
if(d.code){
try{new Function('SCENE','OBJS','CAM','SEL','THREE','toast','refreshOL','refreshSt',d.code)(SCENE,OBJS,CAM,SEL,THREE,toast,refreshOL,refreshSt);
addMsg(`โœ… AI texture applied!\n๐Ÿ–ผ๏ธ <img src="${d.url}" style="max-width:100px;border-radius:4px">`,'ai','g');
}catch(e){addMsg('โš  '+e.message,'ai','e');}
}
}
async function sendTex(){
const inp=document.getElementById('ti'),q=inp.value.trim();if(!q)return;
inp.value='';document.getElementById('tdlg').classList.remove('show');addMsg('๐ŸŽจ Texture: "'+q+'"','user');showTh();
try{
const res=await fetch('/api/texture-search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q,object:SEL?.name||'selected'})});
const data=await res.json();rmTh();
if(data.error){addMsg('โŒ '+data.error,'ai','e');return;}
let code=(data.choices?.[0]?.message?.content||'').trim().replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim();
document.getElementById('sa').value='// Texture: '+q+'\n'+code;
try{new Function('SCENE','OBJS','CAM','SEL','THREE','toast','refreshOL','refreshSt',code)(SCENE,OBJS,CAM,SEL,THREE,toast,refreshOL,refreshSt);addMsg('โœ… Texture applied!','ai','g');}
catch(e){addMsg('โš  '+e.message+'\nCode in ๐Ÿ“ Script','ai','e');}
}catch(e){rmTh();addMsg('โŒ '+e.message,'ai','e');}
}
async function genAnim(){
const desc=document.getElementById('af-d').value.trim();if(!desc){toast('Describe the animation');return;}
const dur=parseInt(document.getElementById('af-dur').value),fps=parseInt(document.getElementById('af-fps').value);
const type=document.getElementById('af-ty').value,loop=document.getElementById('af-lp').checked;
const total=dur*fps;document.getElementById('tlend').value=total;FR_END=total;FR_FPS=fps;
document.getElementById('aaf').classList.remove('show');
const objCtx=OBJS.slice(0,10).map(o=>`"${o.name}"`).join(',');
const prompt=`Create ${type} animation: ${desc}
Objects: ${objCtx}. Frames: ${total} (${dur}s@${fps}fps). Loop: ${loop}.
OUTPUT ONLY RAW JS.
1. Set: document.getElementById('tlend').value='${total}';
2. Easing: const eIO=t=>t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2;
3. For orbits: use requestAnimationFrame + Math.cos/sin (NOT keyframes alone)
4. KF example: if(!KF['N'])KF['N']={};if(!KF['N']['ry'])KF['N']['ry']={};for(let f=0;f<=${total};f+=2){const t=f/${total};KF['N']['ry'][f]=eIO(t)*Math.PI*2;}
5. End: drawTL();applyKF();toast('Animation ready! Press Space');`;
addMsg('๐ŸŽฌ Generating: '+desc,'user');showTh();
try{
const res=await fetch('/api/animation',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:[{role:'user',content:prompt}],max_tokens:2500,temperature:.15})});
const data=await res.json();rmTh();
let code=(data.choices?.[0]?.message?.content||'').trim().replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim();
if(!code){addMsg('โš  No anim code generated','ai','e');return;}
document.getElementById('sa').value='// Animation: '+desc+'\n'+code;
try{new Function('SCENE','OBJS','CAM','KF','FR_FPS','FR_END','addObj','addLight','act','toast','THREE','drawTL','refreshOL','refreshSt','applyKF','setFr','SEL',code)(SCENE,OBJS,CAM,KF,FR_FPS,FR_END,addObj,addLight,act,toast,THREE,drawTL,refreshOL,refreshSt,applyKF,setFr,SEL);addMsg('โœ… Animation ready! Press Space โ–ถ','ai','o');}
catch(e){addMsg('โš  '+e.message+'\nCode in ๐Ÿ“ Script','ai','e');}
}catch(e){rmTh();addMsg('โŒ '+e.message,'ai','e');}
}
</script>
</body>
</html>