Spaces:
Sleeping
Sleeping
| <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 & 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> | |
| ; | |
| 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> |